Sprint 36~39 — IFC Alignment/Camber + proc-macro 스캐폴딩 + 변단면 거더
## 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>
This commit is contained in:
@@ -65,6 +65,10 @@ pub struct SceneParams {
|
||||
/// 데크 헌치 (Haunch) 깊이 [mm]. Sprint 31.
|
||||
/// 거더 상부와 데크 soffit 사이 전환부 두께. 0 = 헌치 없음(데크 = 거더 상부 직접 접촉).
|
||||
pub haunch_depth: f32,
|
||||
/// 변단면 거더 (Variable Depth, Sprint 39).
|
||||
/// 연속교의 경우 지점부가 높고 경간 중앙이 낮은 포물선 변화.
|
||||
/// 값 = 지점 대비 중앙부 단면 높이 **감소량** [mm]. 0 = 상수 단면.
|
||||
pub variable_depth_mm: f32,
|
||||
}
|
||||
|
||||
impl Default for SceneParams {
|
||||
@@ -86,6 +90,7 @@ impl Default for SceneParams {
|
||||
show_diaphragms: true,
|
||||
camber_mid_mm: 0.0,
|
||||
haunch_depth: 0.0,
|
||||
variable_depth_mm: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,6 +159,27 @@ fn translate(mut mesh: Mesh, dx: f32, dy: f32, dz: f32) -> Mesh {
|
||||
mesh
|
||||
}
|
||||
|
||||
/// Sprint 39: 변단면 거더 — 지점부 대비 중앙 단면 높이 감소 (연속교 관례).
|
||||
/// 경간 [z0, z1] 에서 soffit 을 포물선으로 들어올림:
|
||||
/// u = z - z0 ∈ [0, span]
|
||||
/// soffit_lift(u) = 4·max·u·(span-u) / span² (u=span/2 에서 최대)
|
||||
/// 각 정점 Y 를 선형 보간:
|
||||
/// y_new = y + lift(u) · (1 - y/h)
|
||||
/// → y=0(소핏) 에서 lift 만큼 들어올리고, y=h(상면) 에서 0.
|
||||
fn apply_variable_depth(mesh: &mut Mesh, z0: f32, z1: f32, max_mm: f32, girder_h: f32) {
|
||||
if max_mm.abs() < 1e-3 || girder_h <= 0.0 { return; }
|
||||
let span = z1 - z0;
|
||||
if span <= 0.0 { return; }
|
||||
for v in &mut mesh.vertices {
|
||||
let u = v[2] - z0;
|
||||
if u > 0.0 && u < span {
|
||||
let lift = 4.0 * max_mm * u * (span - u) / (span * span);
|
||||
let t = 1.0 - (v[1] / girder_h).clamp(0.0, 1.0);
|
||||
v[1] += lift * t;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sprint 30: Camber(솟음) 포물선 Y 오프셋.
|
||||
/// 경간 [z0, z1] 내에서 중앙에서 `mid_mm` 만큼 위로 솟음.
|
||||
/// z - z0 = u ∈ [0, span]. y_off = 4 · mid · u · (span - u) / span² (중앙 u=span/2 에서 최대 = mid)
|
||||
@@ -261,6 +287,10 @@ pub fn build_bridge_scene<K: GeomKernel>(kernel: &K, p: &SceneParams) -> Result<
|
||||
};
|
||||
let mut mesh = kernel.girder_mesh(&ir)?;
|
||||
mesh.recolor(COL_GIRDER);
|
||||
// Variable depth 는 거더 local 좌표계(Y=0 소핏 기준)에서 먼저 적용, 그 후 translate.
|
||||
apply_variable_depth(
|
||||
&mut mesh, 0.0, span_mm, p.variable_depth_mm, girder_h,
|
||||
);
|
||||
let mut placed = translate(mesh, x, 0.0, z_base);
|
||||
apply_camber_mesh(&mut placed, z_base, z_end, p.camber_mid_mm);
|
||||
parts.push(placed);
|
||||
@@ -607,6 +637,10 @@ pub fn build_selectable_scene<K: GeomKernel>(
|
||||
};
|
||||
let mut mesh = kernel.girder_mesh(&ir)?;
|
||||
mesh.recolor(COL_GIRDER);
|
||||
// Variable depth: local 좌표(Z: 0~span_mm)에서 먼저 적용.
|
||||
apply_variable_depth(
|
||||
&mut mesh, 0.0, span_mm, p.variable_depth_mm, girder_h,
|
||||
);
|
||||
for v in &mut mesh.vertices { v[0] += x; v[2] += z_base; }
|
||||
apply_camber_mesh(&mut mesh, z_base, z_end, p.camber_mid_mm);
|
||||
let label = if span_count > 1 {
|
||||
|
||||
@@ -656,6 +656,8 @@ impl RenderState {
|
||||
.show(ui, |ui| {
|
||||
ps!(ui, "경사각 (°)", &mut p.skew_deg, -30.0..=30.0, 1.0);
|
||||
ps!(ui, "솟음 (mm)", &mut p.camber_mid_mm, 0.0..=200.0, 5.0);
|
||||
// Sprint 39: 변단면 거더
|
||||
ps!(ui, "변단면 (mm)", &mut p.variable_depth_mm, 0.0..=800.0, 20.0);
|
||||
});
|
||||
|
||||
// ── 하부구조 (Substructure) ───────────────────────────
|
||||
|
||||
@@ -47,6 +47,9 @@ pub struct ProjectFile {
|
||||
/// 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 }
|
||||
@@ -81,6 +84,7 @@ impl ProjectFile {
|
||||
show_diaphragms: p.show_diaphragms,
|
||||
camber_mid_mm: p.camber_mid_mm,
|
||||
haunch_depth: p.haunch_depth,
|
||||
variable_depth_mm: p.variable_depth_mm,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +112,7 @@ impl ProjectFile {
|
||||
show_diaphragms: self.show_diaphragms,
|
||||
camber_mid_mm: self.camber_mid_mm,
|
||||
haunch_depth: self.haunch_depth,
|
||||
variable_depth_mm: self.variable_depth_mm,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,5 +168,6 @@ pub fn scene_params_to_ifc(p: &SceneParams, name: &str) -> cimery_ifc::BridgeExp
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user