1use crate::dof::{Dof, Dof6Mask};
25use crate::geom::default_local_ref_vector;
26use crate::ids::{ElemId, MaterialId, NodeId, StoryId};
27use crate::ids::{FloorRegionId, SectionId, SlabId};
28use crate::material_grade::{material_presets, MaterialPreset};
29use crate::model::{
30 default_story_name, Axis, AxisGroup, AxisGroupKind, AxisPlanDir, AxisSource, ElementData,
31 ElementKind, EndCondition, FloorRegion, ForceRegime, LocalAxis, Material, Model, Node, Section,
32 Slab, SlabPlate, SlabShape, SlabUsage, Story,
33};
34
35pub const GRID_TOL_MM: f64 = crate::axis_gen::AXIS_TOL_MM;
37
38#[derive(Clone, Debug, Default, PartialEq)]
44pub struct SpaceGrid {
45 pub x_lines: Vec<GridLine>,
47 pub y_lines: Vec<GridLine>,
49 pub levels: Vec<GridLevel>,
52}
53
54#[derive(Clone, Debug, PartialEq)]
56pub struct GridLine {
57 pub name: String,
58 pub coord: f64,
60}
61
62#[derive(Clone, Debug, PartialEq)]
64pub struct GridLevel {
65 pub name: String,
66 pub elevation: f64,
68 pub story: Option<StoryId>,
70}
71
72impl SpaceGrid {
73 pub fn is_empty(&self) -> bool {
75 self.x_lines.is_empty() || self.y_lines.is_empty() || self.levels.is_empty()
76 }
77
78 pub fn points(&self) -> impl Iterator<Item = [f64; 3]> + '_ {
80 self.x_lines.iter().flat_map(move |gx| {
81 self.y_lines.iter().flat_map(move |gy| {
82 self.levels
83 .iter()
84 .map(move |gz| [gx.coord, gy.coord, gz.elevation])
85 })
86 })
87 }
88}
89
90pub fn space_grid(model: &Model) -> SpaceGrid {
105 let mut x_lines = plan_lines(model, AxisPlanDir::X);
106 let mut y_lines = plan_lines(model, AxisPlanDir::Y);
107 x_lines.sort_by(|a, b| a.coord.total_cmp(&b.coord));
108 y_lines.sort_by(|a, b| a.coord.total_cmp(&b.coord));
109 dedup_lines(&mut x_lines);
110 dedup_lines(&mut y_lines);
111
112 let base = model.base_elevation();
113 let mut levels = vec![GridLevel {
114 name: "GL".to_string(),
115 elevation: base,
116 story: None,
117 }];
118 for s in &model.stories {
119 if (s.elevation - base).abs() <= GRID_TOL_MM {
120 continue;
121 }
122 levels.push(GridLevel {
123 name: s.name.clone(),
124 elevation: s.elevation,
125 story: Some(s.id),
126 });
127 }
128
129 SpaceGrid {
130 x_lines,
131 y_lines,
132 levels,
133 }
134}
135
136fn plan_lines(model: &Model, dir: AxisPlanDir) -> Vec<GridLine> {
138 let mut out = Vec::new();
139 for g in &model.axes {
140 if g.kind.plan_dir() != Some(dir) {
141 continue;
142 }
143 let (AxisGroupKind::Parallel { origin, .. }, Some(off)) = (g.kind, g.kind.offset_dir())
144 else {
145 continue;
146 };
147 let (o, d) = match dir {
150 AxisPlanDir::X => (origin[0], off[0]),
151 AxisPlanDir::Y => (origin[1], off[1]),
152 };
153 for a in &g.axes {
154 let Some(dist) = a.distance else { continue };
155 out.push(GridLine {
156 name: a.name.clone(),
157 coord: o + dist * d,
158 });
159 }
160 }
161 out
162}
163
164fn dedup_lines(lines: &mut Vec<GridLine>) {
166 lines.dedup_by(|b, a| (b.coord - a.coord).abs() <= GRID_TOL_MM);
167}
168
169#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
175pub enum BaseSupport {
176 Fixed,
178 #[default]
180 Pinned,
181}
182
183impl BaseSupport {
184 fn mask(self) -> Dof6Mask {
185 match self {
186 BaseSupport::Fixed => Dof6Mask::FIXED,
187 BaseSupport::Pinned => {
188 let mut m = Dof6Mask::FREE;
189 m.set_fixed(Dof::Ux);
190 m.set_fixed(Dof::Uy);
191 m.set_fixed(Dof::Uz);
192 m
193 }
194 }
195 }
196}
197
198#[derive(Clone, Debug, PartialEq)]
200pub struct FrameSpec {
201 pub origin: [f64; 2],
203 pub base_elevation: f64,
205 pub x_spans: Vec<f64>,
207 pub y_spans: Vec<f64>,
209 pub story_heights: Vec<f64>,
211 pub story_names: Vec<String>,
215 pub x_group_name: String,
217 pub y_group_name: String,
219 pub base_support: BaseSupport,
221 pub with_girders: bool,
223 pub with_slabs: bool,
225 pub slab_usage: Option<SlabUsage>,
227 pub slab_thickness: f64,
229 pub slab_concrete: String,
232}
233
234pub const SLAB_SECTION_NAME: &str = "S15";
236
237pub const DEFAULT_SLAB_CONCRETE: &str = "Fc21";
239
240impl Default for FrameSpec {
241 fn default() -> Self {
242 Self {
243 origin: [0.0, 0.0],
244 base_elevation: 0.0,
245 x_spans: vec![6000.0, 6000.0],
246 y_spans: vec![6000.0],
247 story_heights: vec![4000.0, 3500.0, 3500.0],
248 story_names: Vec::new(),
249 x_group_name: "X".to_string(),
250 y_group_name: "Y".to_string(),
251 base_support: BaseSupport::Pinned,
252 with_girders: true,
253 with_slabs: true,
254 slab_usage: Some(SlabUsage::Office),
255 slab_thickness: 150.0,
256 slab_concrete: DEFAULT_SLAB_CONCRETE.to_string(),
257 }
258 }
259}
260
261impl FrameSpec {
262 pub fn x_coords(&self) -> Vec<f64> {
264 accumulate(self.origin[0], &self.x_spans)
265 }
266
267 pub fn y_coords(&self) -> Vec<f64> {
269 accumulate(self.origin[1], &self.y_spans)
270 }
271
272 pub fn levels(&self) -> Vec<f64> {
274 accumulate(self.base_elevation, &self.story_heights)
275 }
276
277 pub fn validate(&self) -> Option<String> {
282 if self.story_heights.is_empty() {
283 return Some("階高を 1 つ以上入力してください。".into());
284 }
285 if self.x_spans.iter().chain(&self.y_spans).any(|s| *s <= 0.0) {
286 return Some("スパンは正の値で入力してください。".into());
287 }
288 if self.story_heights.iter().any(|h| *h <= 0.0) {
289 return Some("階高は正の値で入力してください。".into());
290 }
291 if self.with_slabs && self.slab_thickness <= 0.0 {
292 return Some("床の板厚は正の値で入力してください。".into());
293 }
294 if self.with_slabs && self.slab_concrete_preset().is_none() {
295 return Some(format!(
296 "床のコンクリート「{}」は標準材料にありません。",
297 self.slab_concrete
298 ));
299 }
300 None
301 }
302
303 fn slab_concrete_preset(&self) -> Option<MaterialPreset> {
305 material_presets()
306 .into_iter()
307 .find(|p| p.name == self.slab_concrete)
308 }
309
310 pub fn counts(&self) -> FrameCounts {
312 let nx = self.x_spans.len() + 1;
313 let ny = self.y_spans.len() + 1;
314 let n_story = self.story_heights.len();
315 let n_level = n_story + 1;
317 let columns = nx * ny * n_story;
318 let per_level = self.x_spans.len() * ny + self.y_spans.len() * nx;
319 let girders = per_level * if self.with_girders { n_level } else { 1 };
321 let slabs = if self.with_slabs {
322 self.x_spans.len() * self.y_spans.len() * n_level
323 } else {
324 0
325 };
326 FrameCounts {
327 nodes: nx * ny * n_level,
328 columns,
329 girders,
330 slabs,
331 }
332 }
333}
334
335#[derive(Clone, Copy, Debug, PartialEq, Eq)]
337pub struct FrameCounts {
338 pub nodes: usize,
339 pub columns: usize,
340 pub girders: usize,
341 pub slabs: usize,
342}
343
344fn accumulate(start: f64, steps: &[f64]) -> Vec<f64> {
346 let mut out = Vec::with_capacity(steps.len() + 1);
347 let mut v = start;
348 out.push(v);
349 for s in steps {
350 v += *s;
351 out.push(v);
352 }
353 out
354}
355
356#[derive(Clone, Debug, PartialEq)]
358pub struct FrameGenResult {
359 pub nodes: Vec<Node>,
360 pub elements: Vec<ElementData>,
361 pub axes: Vec<AxisGroup>,
362 pub stories: Vec<Story>,
363 pub sections: Vec<Section>,
365 pub materials: Vec<Material>,
367 pub floor_regions: Vec<FloorRegion>,
369 pub slabs: Vec<Slab>,
371}
372
373pub fn generate_frame(spec: &FrameSpec) -> Result<FrameGenResult, String> {
403 if let Some(msg) = spec.validate() {
404 return Err(msg);
405 }
406 let xs = spec.x_coords();
407 let ys = spec.y_coords();
408 let zs = spec.levels();
409 let (nx, ny, nz) = (xs.len(), ys.len(), zs.len());
410
411 let nid = |ix: usize, iy: usize, iz: usize| NodeId(((ix * ny + iy) * nz + iz) as u32);
413
414 let mut nodes = Vec::with_capacity(nx * ny * nz);
415 for (ix, x) in xs.iter().enumerate() {
416 for (iy, y) in ys.iter().enumerate() {
417 for (iz, z) in zs.iter().enumerate() {
418 nodes.push(Node {
419 id: nid(ix, iy, iz),
420 coord: [*x, *y, *z],
421 restraint: if iz == 0 {
422 spec.base_support.mask()
423 } else {
424 Dof6Mask::FREE
425 },
426 mass: None,
427 story: None,
428 support_spring: None,
429 });
430 }
431 }
432 }
433
434 let mut elements: Vec<ElementData> = Vec::new();
435 let push_member = |a: NodeId, b: NodeId, vertical: bool, elements: &mut Vec<ElementData>| {
436 elements.push(ElementData {
437 id: ElemId(elements.len() as u32),
438 kind: ElementKind::Beam,
439 nodes: [a, b].into_iter().collect(),
440 local_axis: LocalAxis {
444 ref_vector: default_local_ref_vector(vertical),
445 },
446 section: None,
447 end_cond: [EndCondition::Fixed, EndCondition::Fixed],
448 force_regime: ForceRegime::Auto,
449 rigid_zone: Default::default(),
450 plastic_zone: None,
451 spring: None,
452 });
453 };
454
455 for ix in 0..nx {
457 for iy in 0..ny {
458 for iz in 0..nz - 1 {
459 push_member(nid(ix, iy, iz), nid(ix, iy, iz + 1), true, &mut elements);
460 }
461 }
462 }
463 for iz in 0..nz {
467 if iz > 0 && !spec.with_girders {
468 continue;
469 }
470 for iy in 0..ny {
471 for ix in 0..nx - 1 {
472 push_member(nid(ix, iy, iz), nid(ix + 1, iy, iz), false, &mut elements);
473 }
474 }
475 for ix in 0..nx {
476 for iy in 0..ny - 1 {
477 push_member(nid(ix, iy, iz), nid(ix, iy + 1, iz), false, &mut elements);
478 }
479 }
480 }
481
482 let x_group = AxisGroup {
484 name: spec.x_group_name.clone(),
485 kind: AxisGroupKind::Parallel {
486 origin: spec.origin,
487 angle_deg: 270.0,
488 },
489 axes: xs
490 .iter()
491 .enumerate()
492 .map(|(ix, x)| Axis {
493 name: format!("{}{}", spec.x_group_name, ix + 1),
494 distance: Some(x - spec.origin[0]),
495 nodes: (0..ny)
496 .flat_map(|iy| (0..nz).map(move |iz| nid(ix, iy, iz)))
497 .collect(),
498 source: AxisSource::Manual,
499 })
500 .collect(),
501 };
502 let y_group = AxisGroup {
503 name: spec.y_group_name.clone(),
504 kind: AxisGroupKind::Parallel {
505 origin: spec.origin,
506 angle_deg: 0.0,
507 },
508 axes: ys
509 .iter()
510 .enumerate()
511 .map(|(iy, y)| Axis {
512 name: format!("{}{}", spec.y_group_name, iy + 1),
513 distance: Some(y - spec.origin[1]),
514 nodes: (0..nx)
515 .flat_map(|ix| (0..nz).map(move |iz| nid(ix, iy, iz)))
516 .collect(),
517 source: AxisSource::Manual,
518 })
519 .collect(),
520 };
521
522 let stories = zs
525 .iter()
526 .enumerate()
527 .map(|(si, z)| {
528 let name = spec
529 .story_names
530 .get(si)
531 .map(|s| s.trim().to_string())
532 .filter(|s| !s.is_empty())
533 .unwrap_or_else(|| default_story_name(si));
534 Story {
535 id: StoryId(si as u32),
536 name,
537 elevation: *z,
538 node_ids: Vec::new(),
539 seismic_weight: None,
540 weight_override: None,
541 structure: Default::default(),
542 level_kind: Default::default(),
543 }
544 })
545 .collect();
546
547 let mut sections = Vec::new();
551 let mut materials = Vec::new();
552 let mut floor_regions = Vec::new();
553 let mut slabs = Vec::new();
554 if spec.with_slabs && nx >= 2 && ny >= 2 {
555 let sec_id = SectionId(0);
556 let mat_id = MaterialId(0);
557 let preset = spec
559 .slab_concrete_preset()
560 .expect("床のコンクリートは validate で検査済み");
561 materials.push(Material {
562 id: mat_id,
563 name: preset.name.to_string(),
564 category: preset.category,
565 young: preset.young,
566 poisson: preset.poisson,
567 density: preset.density,
568 shear: None,
569 fc: preset.fc,
570 fy: preset.fy,
571 concrete_class: Default::default(),
572 strength_factor: None,
573 });
574 let mut section = crate::section_shape::SectionShape::RcSlab {
575 thickness: spec.slab_thickness,
576 }
577 .to_section(sec_id, SLAB_SECTION_NAME.to_string());
578 section.material = Some(mat_id);
579 sections.push(section);
580 for iz in 0..nz {
581 for ix in 0..nx - 1 {
582 for iy in 0..ny - 1 {
583 let boundary = vec![
585 nid(ix, iy, iz),
586 nid(ix + 1, iy, iz),
587 nid(ix + 1, iy + 1, iz),
588 nid(ix, iy + 1, iz),
589 ];
590 let region_id = FloorRegionId(floor_regions.len() as u32);
591 let slab_id = SlabId(slabs.len() as u32);
592 slabs.push(Slab {
593 id: slab_id,
594 shape: SlabShape::Enclosed {
595 boundary: boundary.clone(),
596 },
597 plate: SlabPlate {
598 section: Some(sec_id),
599 usage: spec.slab_usage,
600 ..Default::default()
601 },
602 });
603 let mut region = FloorRegion::new(region_id, boundary);
604 region.slab_ids.push(slab_id);
605 floor_regions.push(region);
606 }
607 }
608 }
609 }
610
611 Ok(FrameGenResult {
612 nodes,
613 elements,
614 axes: vec![x_group, y_group],
615 stories,
616 sections,
617 materials,
618 floor_regions,
619 slabs,
620 })
621}
622
623pub fn frame_model(spec: &FrameSpec) -> Result<Model, String> {
628 let gen = generate_frame(spec)?;
629 Ok(Model {
630 nodes: gen.nodes,
631 elements: gen.elements,
632 axes: gen.axes,
633 stories: gen.stories,
634 sections: gen.sections,
635 materials: gen.materials,
636 floor_regions: gen.floor_regions,
637 slabs: gen.slabs,
638 ..Model::with_default_load_cases()
639 })
640}
641
642#[cfg(test)]
643mod tests {
644 use super::*;
645
646 #[test]
648 fn test_generate_frame_counts_and_supports() {
649 let spec = FrameSpec {
650 base_support: BaseSupport::Fixed,
651 ..FrameSpec::default()
652 };
653 let counts = spec.counts();
654 let model = frame_model(&spec).unwrap();
655
656 assert_eq!(model.nodes.len(), 3 * 2 * 4);
658 assert_eq!(model.nodes.len(), counts.nodes);
659 assert_eq!(counts.columns, 6 * 3);
662 assert_eq!(counts.girders, (2 * 2 + 3) * 4);
663 assert_eq!(model.elements.len(), counts.columns + counts.girders);
664
665 let base_beams = model
667 .elements
668 .iter()
669 .filter(|e| {
670 e.nodes.len() == 2
671 && e.nodes
672 .iter()
673 .all(|n| model.nodes[n.index()].coord[2] == 0.0)
674 })
675 .count();
676 assert_eq!(base_beams, 2 * 2 + 3, "基礎梁が基部レベルに架かる");
677
678 let fixed: Vec<&Node> = model
680 .nodes
681 .iter()
682 .filter(|n| n.restraint == Dof6Mask::FIXED)
683 .collect();
684 assert_eq!(fixed.len(), 6);
685 assert!(fixed.iter().all(|n| n.coord[2] == 0.0));
686
687 assert!(model.validate().is_ok(), "{:?}", model.validate());
689 }
690
691 #[test]
693 fn test_generated_members_have_no_section() {
694 let model = frame_model(&FrameSpec::default()).unwrap();
695 assert!(model.elements.iter().all(|e| e.section.is_none()));
696 }
697
698 #[test]
700 fn test_pinned_base_is_default() {
701 let spec = FrameSpec::default();
702 assert_eq!(spec.base_support, BaseSupport::Pinned);
703 let model = frame_model(&spec).unwrap();
704 let base = model.nodes.iter().find(|n| n.coord[2] == 0.0).unwrap();
705 assert!(base.restraint.is_fixed(Dof::Ux));
706 assert!(base.restraint.is_fixed(Dof::Uz));
707 assert!(!base.restraint.is_fixed(Dof::Rx));
708 }
709
710 #[test]
712 fn test_generated_axes_and_stories() {
713 let model = frame_model(&FrameSpec::default()).unwrap();
714 let names: Vec<&str> = model.axes[0].axes.iter().map(|a| a.name.as_str()).collect();
715 assert_eq!(names, vec!["X1", "X2", "X3"]);
716 let dists: Vec<f64> = model.axes[0]
717 .axes
718 .iter()
719 .filter_map(|a| a.distance)
720 .collect();
721 assert_eq!(dists, vec![0.0, 6000.0, 12000.0]);
722 let names: Vec<&str> = model.axes[1].axes.iter().map(|a| a.name.as_str()).collect();
723 assert_eq!(names, vec!["Y1", "Y2"]);
724
725 let stories: Vec<(&str, f64)> = model
727 .stories
728 .iter()
729 .map(|s| (s.name.as_str(), s.elevation))
730 .collect();
731 assert_eq!(
732 stories,
733 vec![("1F", 0.0), ("2F", 4000.0), ("3F", 7500.0), ("4F", 11000.0)]
734 );
735 assert_eq!(model.stories[0].elevation, model.base_elevation());
737 let layers: Vec<(String, f64)> = model
739 .layers()
740 .into_iter()
741 .map(|l| (l.name, l.height))
742 .collect();
743 assert_eq!(
744 layers,
745 vec![
746 ("1F".to_string(), 4000.0),
747 ("2F".to_string(), 3500.0),
748 ("3F".to_string(), 3500.0)
749 ]
750 );
751 }
752
753 #[test]
756 fn test_story_names_from_spec() {
757 let spec = FrameSpec {
758 story_names: vec!["GL".into(), "2FL".into(), " ".into(), "RFL".into()],
759 ..FrameSpec::default()
760 };
761 let model = frame_model(&spec).unwrap();
762 let names: Vec<&str> = model.stories.iter().map(|s| s.name.as_str()).collect();
763 assert_eq!(names, vec!["GL", "2FL", "3F", "RFL"]);
764 }
765
766 #[test]
768 fn test_invalid_spec_is_rejected() {
769 let zero_span = FrameSpec {
770 x_spans: vec![6000.0, 0.0],
771 ..FrameSpec::default()
772 };
773 assert!(frame_model(&zero_span).is_err());
774 let no_story = FrameSpec {
775 story_heights: Vec::new(),
776 ..FrameSpec::default()
777 };
778 assert!(frame_model(&no_story).is_err());
779 let zero_thickness = FrameSpec {
780 slab_thickness: 0.0,
781 ..FrameSpec::default()
782 };
783 assert!(frame_model(&zero_thickness).is_err());
784 let unknown_concrete = FrameSpec {
786 slab_concrete: "Fc999".into(),
787 ..FrameSpec::default()
788 };
789 assert!(frame_model(&unknown_concrete).is_err());
790 let no_slabs = FrameSpec {
792 with_slabs: false,
793 slab_concrete: String::new(),
794 ..FrameSpec::default()
795 };
796 assert!(frame_model(&no_slabs).is_ok());
797 }
798
799 #[test]
804 fn test_generated_slabs_share_one_section() {
805 let spec = FrameSpec::default();
806 let model = frame_model(&spec).unwrap();
807 assert_eq!(model.floor_regions.len(), 2 * 4);
809 assert_eq!(model.floor_regions.len(), spec.counts().slabs);
810 assert_eq!(model.sections.len(), 1, "床の断面は 1 枚だけ");
811 let sec = &model.sections[0];
812 assert_eq!(sec.name, SLAB_SECTION_NAME);
813 assert_eq!(sec.floor, None, "階を持たない断面として作る");
814 assert_eq!(sec.thickness, Some(150.0));
815 assert_eq!(sec.material, Some(crate::ids::MaterialId(0)));
816 assert_eq!(
817 model.slabs.len(),
818 model.floor_regions.len(),
819 "床領域ごとに床板 1 枚"
820 );
821 assert!(model
822 .slabs
823 .iter()
824 .all(|sl| sl.section() == Some(crate::ids::SectionId(0))));
825 assert!(model.slabs.iter().all(|sl| sl.usage() == spec.slab_usage));
826 for sl in &model.floor_regions {
827 let boundary = &sl.boundary;
828 assert_eq!(boundary.len(), 4);
829 let zs: Vec<f64> = boundary
830 .iter()
831 .map(|n| model.nodes[n.index()].coord[2])
832 .collect();
833 assert!(zs.windows(2).all(|w| w[0] == w[1]), "床は水平");
834 }
835 assert!(
836 model
837 .floor_regions
838 .iter()
839 .any(|sl| sl.level(&model) == Some(0.0)),
840 "基部レベルにも床を作る"
841 );
842 assert!(model.validate().is_ok(), "{:?}", model.validate());
843 }
844
845 #[test]
850 fn test_slab_concrete_is_created() {
851 let model = frame_model(&FrameSpec::default()).unwrap();
852 assert_eq!(model.materials.len(), 1, "床のコンクリートだけを作る");
853 let mat = &model.materials[0];
854 assert_eq!(mat.name, DEFAULT_SLAB_CONCRETE);
855 assert_eq!(mat.category, crate::model::MaterialCategory::Concrete);
856 assert_eq!(mat.fc, Some(21.0));
857 assert!(mat.density > 0.0, "自重が 0 にならない");
858 assert!(model.validate().is_ok(), "{:?}", model.validate());
859
860 let spec = FrameSpec {
862 slab_concrete: "Fc30".into(),
863 ..FrameSpec::default()
864 };
865 let model = frame_model(&spec).unwrap();
866 assert_eq!(model.materials[0].name, "Fc30");
867 assert_eq!(model.materials[0].fc, Some(30.0));
868 }
869
870 #[test]
872 fn test_frame_without_slabs() {
873 let spec = FrameSpec {
874 with_slabs: false,
875 ..FrameSpec::default()
876 };
877 let model = frame_model(&spec).unwrap();
878 assert!(model.floor_regions.is_empty());
879 assert!(model.sections.is_empty());
880 assert!(model.materials.is_empty());
881 assert_eq!(spec.counts().slabs, 0);
882 }
883
884 #[test]
889 fn test_foundation_girders_are_always_generated() {
890 let spec = FrameSpec {
891 with_girders: false,
892 ..FrameSpec::default()
893 };
894 let model = frame_model(&spec).unwrap();
895 let at_base = |e: &crate::model::ElementData| {
896 e.nodes.len() == 2
897 && e.nodes
898 .iter()
899 .all(|n| model.nodes[n.index()].coord[2] == 0.0)
900 };
901 let base_beams = model.elements.iter().filter(|e| at_base(e)).count();
902 assert_eq!(base_beams, 2 * 2 + 3, "大梁 OFF でも基礎梁は作る");
903 assert_eq!(spec.counts().girders, base_beams);
904 let upper = model.elements.iter().filter(|e| {
906 e.nodes.len() == 2 && {
907 let a = model.nodes[e.nodes[0].index()].coord[2];
908 let b = model.nodes[e.nodes[1].index()].coord[2];
909 (a - b).abs() < 1e-9 && a > 0.0
910 }
911 });
912 assert_eq!(upper.count(), 0, "大梁 OFF なら基部より上に梁はない");
913 }
914
915 #[test]
920 fn test_generated_frame_is_visible_in_story_frames() {
921 let model = frame_model(&FrameSpec::default()).unwrap();
922 assert!(
923 model.nodes.iter().all(|n| n.story.is_none()),
924 "前提: 生成直後は所属階が未設定(準備計算が埋める派生値)"
925 );
926 for story in &model.stories {
927 let frame =
928 crate::frame::build_frame(&model, crate::frame::FrameTarget::Story(story.id))
929 .expect("階の構面を切り出せる");
930 assert!(
931 frame.elem_count() > 0,
932 "階 {} の伏図に部材が出る",
933 story.name
934 );
935 }
936 let base = crate::frame::build_frame(
938 &model,
939 crate::frame::FrameTarget::Story(model.stories[0].id),
940 )
941 .unwrap();
942 let base_beams = model
943 .elements
944 .iter()
945 .enumerate()
946 .filter(|(i, e)| {
947 base.elem_on[*i]
948 && e.nodes.len() == 2
949 && e.nodes
950 .iter()
951 .all(|n| model.nodes[n.index()].coord[2] == 0.0)
952 })
953 .count();
954 assert_eq!(base_beams, 2 * 2 + 3, "基礎伏図に基礎梁が出る");
955 }
956
957 #[test]
959 fn test_no_slabs_without_grid_region() {
960 let spec = FrameSpec {
961 y_spans: Vec::new(),
962 ..FrameSpec::default()
963 };
964 let model = frame_model(&spec).unwrap();
965 assert!(model.floor_regions.is_empty());
966 assert!(model.sections.is_empty());
967 assert!(model.materials.is_empty());
968 }
969
970 #[test]
973 fn test_space_grid_round_trips_generated_frame() {
974 let model = frame_model(&FrameSpec::default()).unwrap();
975 let grid = super::space_grid(&model);
976 let xs: Vec<f64> = grid.x_lines.iter().map(|l| l.coord).collect();
977 assert_eq!(xs, vec![0.0, 6000.0, 12000.0]);
978 let ys: Vec<f64> = grid.y_lines.iter().map(|l| l.coord).collect();
979 assert_eq!(ys, vec![0.0, 6000.0]);
980 let levels: Vec<(&str, f64)> = grid
981 .levels
982 .iter()
983 .map(|l| (l.name.as_str(), l.elevation))
984 .collect();
985 assert_eq!(
986 levels,
987 vec![("GL", 0.0), ("2F", 4000.0), ("3F", 7500.0), ("4F", 11000.0)]
988 );
989 assert_eq!(grid.points().count(), 24);
991 assert!(!grid.is_empty());
992 }
993
994 #[test]
996 fn test_space_grid_without_axes_is_empty() {
997 let model = Model::default();
998 assert!(super::space_grid(&model).is_empty());
999 }
1000
1001 #[test]
1004 fn test_space_grid_handles_reversed_offset_direction() {
1005 let mut model = frame_model(&FrameSpec::default()).unwrap();
1006 model.axes = vec![
1007 AxisGroup {
1008 name: "X".into(),
1009 kind: AxisGroupKind::Parallel {
1010 origin: [0.0, 0.0],
1011 angle_deg: 90.0,
1012 },
1013 axes: vec![
1014 Axis {
1015 name: "X3".into(),
1016 distance: Some(-12000.0),
1017 nodes: Vec::new(),
1018 source: AxisSource::Manual,
1019 },
1020 Axis {
1021 name: "X1".into(),
1022 distance: Some(0.0),
1023 nodes: Vec::new(),
1024 source: AxisSource::Manual,
1025 },
1026 ],
1027 },
1028 AxisGroup {
1029 name: "Y".into(),
1030 kind: AxisGroupKind::Parallel {
1031 origin: [0.0, 0.0],
1032 angle_deg: 0.0,
1033 },
1034 axes: vec![Axis {
1035 name: "Y1".into(),
1036 distance: Some(0.0),
1037 nodes: Vec::new(),
1038 source: AxisSource::Manual,
1039 }],
1040 },
1041 ];
1042 let grid = super::space_grid(&model);
1043 let xs: Vec<(&str, f64)> = grid
1044 .x_lines
1045 .iter()
1046 .map(|l| (l.name.as_str(), l.coord))
1047 .collect();
1048 assert_eq!(xs, vec![("X1", 0.0), ("X3", 12000.0)]);
1049 }
1050
1051 #[test]
1053 fn test_space_grid_merges_duplicate_lines() {
1054 let mut model = frame_model(&FrameSpec::default()).unwrap();
1055 let dup = model.axes[0].axes[1].clone();
1056 model.axes[0].axes.push(Axis {
1057 name: "X2b".into(),
1058 ..dup
1059 });
1060 let grid = super::space_grid(&model);
1061 let xs: Vec<(&str, f64)> = grid
1062 .x_lines
1063 .iter()
1064 .map(|l| (l.name.as_str(), l.coord))
1065 .collect();
1066 assert_eq!(xs, vec![("X1", 0.0), ("X2", 6000.0), ("X3", 12000.0)]);
1067 }
1068}