//! cimery project file — JSON save/load of SceneParams. //! //! Format: `cimery/projects/*.cimery.json` //! Sprint 12: SceneParams only. Sprint 13+: Feature IRs + Alignment. use serde::{Deserialize, Serialize}; use super::bridge_scene::{GirderSectionType, SceneParams}; // ─── Serialisable form of SceneParams ──────────────────────────────────────── #[allow(dead_code)] #[derive(Serialize, Deserialize)] struct SectionTypeStr(String); #[derive(Serialize, Deserialize)] pub struct ProjectFile { pub version: u32, pub name: String, pub span_m: f64, pub girder_count: usize, pub girder_spacing: f32, pub girder_height: f32, pub slab_thickness: f32, pub section_type: String, // "psc_i" | "steel_box" pub show_alignment: bool, /// Sprint 19 #[serde(default = "default_true")] pub show_cross_beams: bool, #[serde(default = "default_cross_beam_interval")] pub cross_beam_interval_m: f64, #[serde(default = "default_true")] pub show_expansion_joints: bool, /// Sprint 26: 다경간 #[serde(default = "default_span_count")] pub span_count: usize, #[serde(default = "default_pier_type")] pub pier_type: String, // "single" | "multi" /// Sprint 27: 경사각 [deg] #[serde(default)] pub skew_deg: f32, /// Sprint 29: 격벽 표시 #[serde(default = "default_true")] pub show_diaphragms: bool, /// Sprint 30: 솟음(Camber) 중앙값 [mm] #[serde(default)] pub camber_mid_mm: f32, /// Sprint 31: 데크 헌치(Haunch) 깊이 [mm] #[serde(default)] pub haunch_depth: f32, /// Sprint 39: 변단면 거더 중앙부 높이 감소 [mm] #[serde(default)] pub variable_depth_mm: f32, } fn default_true() -> bool { true } fn default_cross_beam_interval() -> f64 { 5.0 } fn default_span_count() -> usize { 1 } fn default_pier_type() -> String { "single".into() } impl ProjectFile { pub fn from_params(name: &str, p: &SceneParams) -> Self { Self { version: 2, name: name.to_owned(), span_m: p.span_m, girder_count: p.girder_count, girder_spacing: p.girder_spacing, girder_height: p.girder_height, slab_thickness: p.slab_thickness, section_type: match p.section_type { GirderSectionType::PscI => "psc_i".into(), GirderSectionType::SteelBox => "steel_box".into(), }, show_alignment: p.show_alignment, show_cross_beams: p.show_cross_beams, cross_beam_interval_m: p.cross_beam_interval_m, show_expansion_joints: p.show_expansion_joints, span_count: p.span_count, pier_type: match p.pier_type { cimery_core::PierType::MultiColumn => "multi".into(), _ => "single".into(), }, skew_deg: p.skew_deg, show_diaphragms: p.show_diaphragms, camber_mid_mm: p.camber_mid_mm, haunch_depth: p.haunch_depth, variable_depth_mm: p.variable_depth_mm, } } pub fn to_params(&self) -> SceneParams { SceneParams { span_m: self.span_m, girder_count: self.girder_count, girder_spacing: self.girder_spacing, girder_height: self.girder_height, slab_thickness: self.slab_thickness, section_type: match self.section_type.as_str() { "steel_box" => GirderSectionType::SteelBox, _ => GirderSectionType::PscI, }, show_alignment: self.show_alignment, show_cross_beams: self.show_cross_beams, cross_beam_interval_m: self.cross_beam_interval_m, show_expansion_joints: self.show_expansion_joints, span_count: self.span_count, pier_type: match self.pier_type.as_str() { "multi" => cimery_core::PierType::MultiColumn, _ => cimery_core::PierType::SingleColumn, }, skew_deg: self.skew_deg, show_diaphragms: self.show_diaphragms, camber_mid_mm: self.camber_mid_mm, haunch_depth: self.haunch_depth, variable_depth_mm: self.variable_depth_mm, } } pub fn save(&self, path: &std::path::Path) -> std::io::Result<()> { let json = serde_json::to_string_pretty(self) .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; std::fs::write(path, json) } pub fn load(path: &std::path::Path) -> std::io::Result { let json = std::fs::read_to_string(path)?; serde_json::from_str(&json) .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) } } /// Default project save path (relative to cimery workspace root). pub fn default_save_path(name: &str) -> std::path::PathBuf { let mut p = std::path::PathBuf::from("projects"); std::fs::create_dir_all(&p).ok(); p.push(format!("{}.cimery.json", name)); p } /// IFC 파일 기본 저장 경로. pub fn default_ifc_path(name: &str) -> std::path::PathBuf { let mut p = std::path::PathBuf::from("projects"); std::fs::create_dir_all(&p).ok(); p.push(format!("{}.ifc", name)); p } /// SceneParams → cimery-ifc::BridgeExportParams 변환. /// /// viewer 의 SceneParams 에 있는 모든 파라미터를 IFC 익스포터 입력으로 매핑. /// 선형(alignment)·camber 는 IFC Phase 3 로드맵(미반영). pub fn scene_params_to_ifc(p: &SceneParams, name: &str) -> cimery_ifc::BridgeExportParams { use cimery_ifc::{BridgeExportParams, IfcSectionKind}; BridgeExportParams { name: name.to_owned(), span_m: p.span_m, span_count: p.span_count, girder_count: p.girder_count, girder_spacing: p.girder_spacing as f64, girder_height: p.girder_height as f64, slab_thickness: p.slab_thickness as f64, bearing_height: 60.0, section_kind: match p.section_type { GirderSectionType::PscI => IfcSectionKind::PscI, GirderSectionType::SteelBox => IfcSectionKind::SteelBox, }, skew_deg: p.skew_deg as f64, haunch_depth: p.haunch_depth as f64, show_parapets: true, show_joints: p.show_expansion_joints, camber_mid_mm: p.camber_mid_mm as f64, } }