## Sprint 36: IfcAlignment (IFC Phase 3b)
- write_straight_alignment(): 직선 horizontal(.LINE.) + 평지
vertical(.CONSTANTGRADIENT.) 세그먼트 + IfcRelNests 계층.
- IfcAlignmentSegment × IfcAlignmentHorizontalSegment × IfcAlignmentVerticalSegment.
- Site aggregate 에 Bridge 와 Alignment 동시 포함.
## Sprint 37: IFC 거더 Camber 반영
- BridgeExportParams.camber_mid_mm 추가.
- camber > 0 일 때: 거더 1 개를 CAMBER_SEGMENTS(10) 세그먼트로 분할, 각 세그먼트
Y 에 포물선 값 적용 → 곡선 거더 근사. Pset 는 첫 세그먼트에 한 번만 부착.
- viewer scene_params_to_ifc() 에서 camber_mid_mm 매핑.
## Sprint 38: cimery-macros 크레이트 (proc-macro 스캐폴딩)
- 신규 크레이트, proc-macro = true, deps: syn/quote/proc-macro2.
- #[derive(ParamSummary)] 구현:
· 구조체 named field 의 PARAM_COUNT (usize) + PARAM_NAMES (&[&str]) 생성.
· 선언 순서 보존, 빈 구조체 지원, tuple/enum 은 컴파일 에러.
- 테스트 3개 (tests/derive_test.rs).
- ADR-002 D 로드맵: #[param(unit, range, default)] 전면 attribute 는 후속.
## Sprint 39: 변단면 거더 (Variable Depth)
- SceneParams.variable_depth_mm (0~800mm) 추가.
- apply_variable_depth(mesh, z0, z1, max, girder_h):
· lift(u) = 4·max·u·(span-u)/span² (중앙에서 최대)
· y_new = y + lift(u)·(1 - y/h)
· 상면 y=h 는 고정, 소핏 y=0 을 최대 lift 만큼 올림. 연속교 중앙부
web 축소 관례와 정합. camber 와 독립 조합 가능.
- build_bridge_scene / build_selectable_scene 거더 생성 루프에 각각 적용
(거더 local 좌표계에서 먼저 → translate → camber 순).
- UI "변단면 (mm)" 슬라이더 (선형·기하 섹션).
- ProjectFile variable_depth_mm 필드 (default 0).
모든 테스트 통과: kernel 18 + ifc 20 + macros 3.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
174 lines
6.9 KiB
Rust
174 lines
6.9 KiB
Rust
//! 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<Self> {
|
|
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,
|
|
}
|
|
}
|