diff --git a/PROGRESS.md b/PROGRESS.md index 600df4c..3e75ac0 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -12,6 +12,11 @@ ## 타임라인 ### 2026-04-15 (계속) +- code — Sprint 35: IFC 뷰어 통합 + Pset_BeamCommon. + - `cimery-viewer` 에 `cimery-ifc` 의존성 추가. `project_file::scene_params_to_ifc()` 변환 함수 (SceneParams → BridgeExportParams 전 필드 매핑). + - 프로젝트 섹션에 "📤 IFC4X3 익스포트" 버튼. `projects/bridge.ifc` 로 저장, 현재 파라미터(경간 수·교각 형식·skew·헌치·단면 등) 그대로 반영. + - `write_pset_beam_common()` 추가: Reference(이름) + Span(mm) + LoadBearing + IsExternal 4 속성, `IFCRELDEFINESBYPROPERTIES` 로 거더 각 beam 에 연결. + - `IfcSectionKind` public re-export. 테스트 17개 통과. - code — Sprint 34: IFC4X3 Add2 익스포터 Phase 2. 정확도·커버리지 확장. - PSC-I 실제 14점 단면 `IFCARBITRARYCLOSEDPROFILEDEF` + `IFCPOLYLINE` 구현 (도심 중심화 Y 평행이동). `IfcSectionKind` enum 으로 단면 종류 분기. - Skew 회전 `write_local_placement_skewed()`: `IFCAXIS2PLACEMENT3D` RefDirection 을 Y축 회전 X축으로 설정. 교대·피어·받침·신축이음에 적용. 거더·데크는 직선 유지. diff --git a/cimery/crates/ifc/src/bridge_export.rs b/cimery/crates/ifc/src/bridge_export.rs index 2fbdbfe..75c2587 100644 --- a/cimery/crates/ifc/src/bridge_export.rs +++ b/cimery/crates/ifc/src/bridge_export.rs @@ -154,16 +154,19 @@ pub fn export_bridge(p: &BridgeExportParams) -> String { let profile = write_girder_profile(&mut w, p.section_kind, p.girder_height); let shape = write_extrude_shape(&mut w, geom_ctx, profile, span_mm); let beam = w.alloc(); + let girder_label = format!("Girder S{}-G{}", s + 1, i + 1); w.emit( beam, &format!( "IFCBEAM({},$,{},$,$,{},{},$,.BEAM.)", lit(&new_ifc_guid()), - lit(&format!("Girder S{}-G{}", s + 1, i + 1)), + lit(&girder_label), placement, shape, ), ); + // Pset_BeamCommon (Sprint 35). + write_pset_beam_common(&mut w, beam, &girder_label, span_mm); elements.push(beam); } } @@ -438,6 +441,46 @@ fn write_girder_profile( } } +/// Pset_BeamCommon 생성 + `IfcRelDefinesByProperties` 로 beam 에 연결 (Sprint 35 Phase 3a). +/// +/// # 속성 +/// - `Reference`: 거더 식별자 (프로젝트별 시퀀스) +/// - `Span`: 경간 길이 [mm] — IfcLengthMeasure +/// - `LoadBearing`: `.T.` (true) +/// - `IsExternal`: `.F.` +fn write_pset_beam_common( + w: &mut IfcWriter, + beam: Ref, + reference: &str, + span_mm: f64, +) { + let p_ref = w.write(&format!( + "IFCPROPERTYSINGLEVALUE('Reference',$,IFCIDENTIFIER({}),$)", + lit(reference), + )); + let p_span = w.write(&format!( + "IFCPROPERTYSINGLEVALUE('Span',$,IFCLENGTHMEASURE({}),$)", + real(span_mm), + )); + let p_lb = w.write( + "IFCPROPERTYSINGLEVALUE('LoadBearing',$,IFCBOOLEAN(.T.),$)", + ); + let p_ext = w.write( + "IFCPROPERTYSINGLEVALUE('IsExternal',$,IFCBOOLEAN(.F.),$)", + ); + let pset = w.write(&format!( + "IFCPROPERTYSET({},$,'Pset_BeamCommon',$,{})", + lit(&new_ifc_guid()), + ref_list(&[p_ref, p_span, p_lb, p_ext]), + )); + w.write(&format!( + "IFCRELDEFINESBYPROPERTIES({},$,$,$,({}),{})", + lit(&new_ifc_guid()), + beam, + pset, + )); +} + /// Skew 회전 + 평행이동 LocalPlacement — 지점부 요소(교대·피어·받침·joint)에 적용. /// skew_rad 는 Y축 중심 회전, pivot_z 기준 반지름 오프셋은 placement origin 에 반영. fn write_local_placement_skewed( @@ -568,6 +611,16 @@ mod tests { assert_ne!(zero, skewed); } + #[test] + fn pset_beam_common_attached_to_girders() { + let ifc = export_bridge(&BridgeExportParams::default()); + assert!(ifc.contains("IFCPROPERTYSET"), "no Pset emitted"); + assert!(ifc.contains("'Pset_BeamCommon'"), "Pset name missing"); + assert!(ifc.contains("IFCRELDEFINESBYPROPERTIES"), "Pset not attached"); + assert!(ifc.contains("LoadBearing")); + assert!(ifc.contains("Span")); + } + #[test] fn haunch_moves_deck_up() { // haunch 0 vs 200 → deck slab 중심 Y 위치가 다름. ifc text diff 로 확인. diff --git a/cimery/crates/ifc/src/lib.rs b/cimery/crates/ifc/src/lib.rs index 7fb73a6..e18e04e 100644 --- a/cimery/crates/ifc/src/lib.rs +++ b/cimery/crates/ifc/src/lib.rs @@ -27,5 +27,5 @@ pub mod writer; pub mod guid; pub mod bridge_export; -pub use bridge_export::{BridgeExportParams, export_bridge}; +pub use bridge_export::{BridgeExportParams, IfcSectionKind, export_bridge}; pub use writer::IfcWriter; diff --git a/cimery/crates/viewer/Cargo.toml b/cimery/crates/viewer/Cargo.toml index 8f97013..9a95d87 100644 --- a/cimery/crates/viewer/Cargo.toml +++ b/cimery/crates/viewer/Cargo.toml @@ -19,6 +19,7 @@ path = "src/main.rs" [dependencies] cimery-kernel = { workspace = true } +cimery-ifc = { workspace = true } log = { workspace = true } env_logger = { workspace = true } wgpu = "22" diff --git a/cimery/crates/viewer/src/lib.rs b/cimery/crates/viewer/src/lib.rs index 779976f..854b0ea 100644 --- a/cimery/crates/viewer/src/lib.rs +++ b/cimery/crates/viewer/src/lib.rs @@ -764,6 +764,18 @@ impl RenderState { } } }); + // Sprint 35: IFC4X3 Add2 익스포트 (현재 파라미터 기준). + if ui.button("📤 IFC4X3 익스포트").clicked() { + let params = project_file::scene_params_to_ifc(&p, "bridge"); + let ifc = cimery_ifc::export_bridge(¶ms); + let path = project_file::default_ifc_path("bridge"); + match std::fs::write(&path, &ifc) { + Ok(_) => log::info!( + "IFC exported: {:?} ({} bytes)", path, ifc.len() + ), + Err(e) => log::error!("IFC export failed: {e}"), + } + } }); ui.separator(); diff --git a/cimery/crates/viewer/src/project_file.rs b/cimery/crates/viewer/src/project_file.rs index 5f61b0d..a5986d7 100644 --- a/cimery/crates/viewer/src/project_file.rs +++ b/cimery/crates/viewer/src/project_file.rs @@ -131,3 +131,37 @@ pub fn default_save_path(name: &str) -> std::path::PathBuf { 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, + } +}