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:
@@ -50,8 +50,15 @@ pub struct BridgeExportParams {
|
||||
pub haunch_depth: f64, // mm
|
||||
pub show_parapets: bool,
|
||||
pub show_joints: bool,
|
||||
/// Sprint 37: 거더 중앙 camber 솟음량 [mm]. 0 = 직선.
|
||||
/// > 0 일 때는 beam 을 `CAMBER_SEGMENTS` 개로 분할해 포물선 근사.
|
||||
pub camber_mid_mm: f64,
|
||||
}
|
||||
|
||||
/// Sprint 37: camber > 0 일 때 각 거더를 N 개 세그먼트로 분할.
|
||||
/// 각 세그먼트는 독립 IfcBeam(extrude) 로 Y 오프셋 차등 적용.
|
||||
const CAMBER_SEGMENTS: usize = 10;
|
||||
|
||||
impl Default for BridgeExportParams {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -68,6 +75,7 @@ impl Default for BridgeExportParams {
|
||||
haunch_depth: 0.0,
|
||||
show_parapets: true,
|
||||
show_joints: true,
|
||||
camber_mid_mm: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,18 +123,28 @@ pub fn export_bridge(p: &BridgeExportParams) -> String {
|
||||
),
|
||||
);
|
||||
|
||||
// Project aggregates Site, Site aggregates Bridge
|
||||
// Project aggregates Site, Site aggregates Bridge + Alignment
|
||||
w.write(&format!(
|
||||
"IFCRELAGGREGATES({},$,$,$,{},({}))",
|
||||
lit(&new_ifc_guid()),
|
||||
project,
|
||||
site,
|
||||
));
|
||||
|
||||
// Sprint 36: IfcAlignment — 교량 선형(직선 horizontal + 평지 vertical).
|
||||
// Phase 3b 단순화: 교량 시점(0,0,0) 에서 total_mm 만큼 직선, 경사 0.
|
||||
let alignment = write_straight_alignment(
|
||||
&mut w, world_placement, geom_ctx,
|
||||
p.span_m * p.span_count as f64 * 1_000.0,
|
||||
);
|
||||
|
||||
// Site 가 Bridge 와 Alignment 를 동시에 집계.
|
||||
w.write(&format!(
|
||||
"IFCRELAGGREGATES({},$,$,$,{},({}))",
|
||||
"IFCRELAGGREGATES({},$,$,$,{},({},{}))",
|
||||
lit(&new_ifc_guid()),
|
||||
site,
|
||||
bridge,
|
||||
alignment,
|
||||
));
|
||||
|
||||
// ── Bridge elements ───────────────────────────────────────────────────
|
||||
@@ -137,37 +155,60 @@ pub fn export_bridge(p: &BridgeExportParams) -> String {
|
||||
let total_mm = span_mm * span_count as f64;
|
||||
|
||||
let skew_rad = p.skew_deg.to_radians();
|
||||
let use_camber = p.camber_mid_mm.abs() > 0.1;
|
||||
|
||||
// Girders (span_count × girder_count) — Phase 2: PSC-I 실제 단면.
|
||||
// 거더는 직선 유지(precast 관례) → skew 미적용.
|
||||
// Girders (span_count × girder_count) — 거더 직선 유지(precast 관례).
|
||||
// Sprint 37: camber 있으면 CAMBER_SEGMENTS 분할 근사.
|
||||
for s in 0..span_count {
|
||||
let z0 = span_mm * s as f64;
|
||||
for i in 0..p.girder_count {
|
||||
let x = (i as f64 - (p.girder_count as f64 - 1.0) * 0.5) * p.girder_spacing;
|
||||
let placement = write_local_placement(
|
||||
&mut w,
|
||||
world_placement,
|
||||
x,
|
||||
p.bearing_height + p.girder_height * 0.5,
|
||||
z0 + span_mm * 0.5,
|
||||
);
|
||||
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 beam_center_y = p.bearing_height + p.girder_height * 0.5;
|
||||
let girder_label = format!("Girder S{}-G{}", s + 1, i + 1);
|
||||
w.emit(
|
||||
beam,
|
||||
&format!(
|
||||
|
||||
if !use_camber {
|
||||
// 직선 — 단일 extrude.
|
||||
let placement = write_local_placement(
|
||||
&mut w, world_placement,
|
||||
x, beam_center_y, z0 + span_mm * 0.5,
|
||||
);
|
||||
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();
|
||||
w.emit(beam, &format!(
|
||||
"IFCBEAM({},$,{},$,$,{},{},$,.BEAM.)",
|
||||
lit(&new_ifc_guid()),
|
||||
lit(&girder_label),
|
||||
placement,
|
||||
shape,
|
||||
),
|
||||
);
|
||||
// Pset_BeamCommon (Sprint 35).
|
||||
write_pset_beam_common(&mut w, beam, &girder_label, span_mm);
|
||||
elements.push(beam);
|
||||
lit(&new_ifc_guid()), lit(&girder_label), placement, shape,
|
||||
));
|
||||
write_pset_beam_common(&mut w, beam, &girder_label, span_mm);
|
||||
elements.push(beam);
|
||||
} else {
|
||||
// Camber — N 세그먼트로 포물선 Y 오프셋 근사.
|
||||
let seg_len = span_mm / CAMBER_SEGMENTS as f64;
|
||||
for k in 0..CAMBER_SEGMENTS {
|
||||
let u_mid = (k as f64 + 0.5) * seg_len; // 세그먼트 중심 u
|
||||
let y_off = 4.0 * p.camber_mid_mm * u_mid * (span_mm - u_mid)
|
||||
/ (span_mm * span_mm);
|
||||
let placement = write_local_placement(
|
||||
&mut w, world_placement,
|
||||
x, beam_center_y + y_off,
|
||||
z0 + (k as f64 + 0.5) * seg_len,
|
||||
);
|
||||
let profile = write_girder_profile(&mut w, p.section_kind, p.girder_height);
|
||||
let shape = write_extrude_shape(&mut w, geom_ctx, profile, seg_len);
|
||||
let beam = w.alloc();
|
||||
w.emit(beam, &format!(
|
||||
"IFCBEAM({},$,{},$,$,{},{},$,.BEAM.)",
|
||||
lit(&new_ifc_guid()),
|
||||
lit(&format!("{}-seg{}", girder_label, k + 1)),
|
||||
placement, shape,
|
||||
));
|
||||
// Pset 는 첫 세그먼트에만 (전체 거더 속성으로 처리).
|
||||
if k == 0 {
|
||||
write_pset_beam_common(&mut w, beam, &girder_label, span_mm);
|
||||
}
|
||||
elements.push(beam);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,6 +482,96 @@ fn write_girder_profile(
|
||||
}
|
||||
}
|
||||
|
||||
/// Sprint 36 Phase 3b: 직선 수평·평지 수직 IfcAlignment 생성.
|
||||
///
|
||||
/// # 구조 (IFC4X3)
|
||||
/// ```text
|
||||
/// IfcAlignment
|
||||
/// └ IfcRelNests
|
||||
/// ├ IfcAlignmentHorizontal
|
||||
/// │ └ IfcRelNests → IfcAlignmentSegment(LINE)
|
||||
/// └ IfcAlignmentVertical
|
||||
/// └ IfcRelNests → IfcAlignmentSegment(CONSTANTGRADIENT)
|
||||
/// ```
|
||||
///
|
||||
/// Phase 3b 단순화: 직선·평지 고정. curve/grade 는 SceneParams 확장 후 Phase 3c.
|
||||
fn write_straight_alignment(
|
||||
w: &mut IfcWriter,
|
||||
world_placement: Ref,
|
||||
_geom_ctx: Ref,
|
||||
total_mm: f64,
|
||||
) -> Ref {
|
||||
// ── Horizontal alignment segment (LINE) ───────────────────────────────
|
||||
let start_pt = w.write(&format!("IFCCARTESIANPOINT(({},{}))", real(0.0), real(0.0)));
|
||||
let h_seg_def = w.write(&format!(
|
||||
"IFCALIGNMENTHORIZONTALSEGMENT($,$,{},{},$,{},{},$,.LINE.)",
|
||||
start_pt, // StartPoint (2D)
|
||||
real(0.0), // StartDirection [rad]
|
||||
real(total_mm), // SegmentLength
|
||||
real(0.0), // StartRadiusOfCurvature (0 = line)
|
||||
));
|
||||
let h_seg = w.write(&format!(
|
||||
"IFCALIGNMENTSEGMENT({},$,$,$,$,{},$,{})",
|
||||
lit(&new_ifc_guid()),
|
||||
world_placement,
|
||||
h_seg_def,
|
||||
));
|
||||
let horizontal = w.write(&format!(
|
||||
"IFCALIGNMENTHORIZONTAL({},$,'Horizontal',$,$,{},$)",
|
||||
lit(&new_ifc_guid()),
|
||||
world_placement,
|
||||
));
|
||||
w.write(&format!(
|
||||
"IFCRELNESTS({},$,$,$,{},({}))",
|
||||
lit(&new_ifc_guid()),
|
||||
horizontal,
|
||||
h_seg,
|
||||
));
|
||||
|
||||
// ── Vertical alignment segment (constant gradient = 0) ────────────────
|
||||
let v_seg_def = w.write(&format!(
|
||||
"IFCALIGNMENTVERTICALSEGMENT($,$,{},{},{},{},{},$,.CONSTANTGRADIENT.)",
|
||||
real(0.0), // StartDistAlong
|
||||
real(total_mm), // HorizontalLength
|
||||
real(0.0), // StartHeight
|
||||
real(0.0), // StartGradient
|
||||
real(0.0), // EndGradient
|
||||
));
|
||||
let v_seg = w.write(&format!(
|
||||
"IFCALIGNMENTSEGMENT({},$,$,$,$,{},$,{})",
|
||||
lit(&new_ifc_guid()),
|
||||
world_placement,
|
||||
v_seg_def,
|
||||
));
|
||||
let vertical = w.write(&format!(
|
||||
"IFCALIGNMENTVERTICAL({},$,'Vertical',$,$,{},$)",
|
||||
lit(&new_ifc_guid()),
|
||||
world_placement,
|
||||
));
|
||||
w.write(&format!(
|
||||
"IFCRELNESTS({},$,$,$,{},({}))",
|
||||
lit(&new_ifc_guid()),
|
||||
vertical,
|
||||
v_seg,
|
||||
));
|
||||
|
||||
// ── Alignment aggregate ───────────────────────────────────────────────
|
||||
let alignment = w.write(&format!(
|
||||
"IFCALIGNMENT({},$,'Bridge Alignment',$,$,{},$,.USERDEFINED.,$)",
|
||||
lit(&new_ifc_guid()),
|
||||
world_placement,
|
||||
));
|
||||
w.write(&format!(
|
||||
"IFCRELNESTS({},$,$,$,{},({},{}))",
|
||||
lit(&new_ifc_guid()),
|
||||
alignment,
|
||||
horizontal,
|
||||
vertical,
|
||||
));
|
||||
|
||||
alignment
|
||||
}
|
||||
|
||||
/// Pset_BeamCommon 생성 + `IfcRelDefinesByProperties` 로 beam 에 연결 (Sprint 35 Phase 3a).
|
||||
///
|
||||
/// # 속성
|
||||
@@ -611,6 +742,45 @@ mod tests {
|
||||
assert_ne!(zero, skewed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn camber_zero_produces_single_beam_per_girder() {
|
||||
let p = BridgeExportParams {
|
||||
camber_mid_mm: 0.0,
|
||||
girder_count: 1,
|
||||
span_count: 1,
|
||||
..Default::default()
|
||||
};
|
||||
let ifc = export_bridge(&p);
|
||||
// 거더 1개 + 경간 1 → IFCBEAM 1 개.
|
||||
assert_eq!(ifc.matches("IFCBEAM(").count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn camber_positive_subdivides_beam() {
|
||||
let p = BridgeExportParams {
|
||||
camber_mid_mm: 50.0,
|
||||
girder_count: 1,
|
||||
span_count: 1,
|
||||
..Default::default()
|
||||
};
|
||||
let ifc = export_bridge(&p);
|
||||
// 10 세그먼트 → IFCBEAM 10 개.
|
||||
assert_eq!(ifc.matches("IFCBEAM(").count(), CAMBER_SEGMENTS);
|
||||
// Pset 는 첫 세그먼트 1개만.
|
||||
assert_eq!(ifc.matches("'Pset_BeamCommon'").count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alignment_present_in_output() {
|
||||
let ifc = export_bridge(&BridgeExportParams::default());
|
||||
assert!(ifc.contains("IFCALIGNMENT("), "IfcAlignment missing");
|
||||
assert!(ifc.contains("IFCALIGNMENTHORIZONTAL("), "horizontal missing");
|
||||
assert!(ifc.contains("IFCALIGNMENTVERTICAL("), "vertical missing");
|
||||
assert!(ifc.contains("IFCALIGNMENTSEGMENT("), "segment missing");
|
||||
assert!(ifc.contains(".LINE."), "horizontal LINE predefined type missing");
|
||||
assert!(ifc.contains(".CONSTANTGRADIENT."), "vertical CONSTANTGRADIENT missing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pset_beam_common_attached_to_girders() {
|
||||
let ifc = export_bridge(&BridgeExportParams::default());
|
||||
|
||||
Reference in New Issue
Block a user