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:
@@ -12,6 +12,11 @@
|
|||||||
## 타임라인
|
## 타임라인
|
||||||
|
|
||||||
### 2026-04-15 (계속)
|
### 2026-04-15 (계속)
|
||||||
|
- code — Sprint 36~39: IFC Alignment + Camber + proc-macro 스캐폴딩 + 변단면 거더.
|
||||||
|
- Sprint 36 (IFC Phase 3b): IfcAlignment + 직선 horizontal/constantgradient vertical segment + IfcRelNests 계층. `write_straight_alignment()` helper. Site 가 Bridge·Alignment 동시 aggregate.
|
||||||
|
- Sprint 37 (IFC Camber): `BridgeExportParams.camber_mid_mm` 추가. camber > 0 일 때 거더를 `CAMBER_SEGMENTS`(=10)개 세그먼트로 분할, 각 세그먼트 Y 오프셋에 포물선 값 적용. Pset 는 첫 세그먼트에만 부착(전체 거더 대표). viewer scene_params_to_ifc 에 매핑.
|
||||||
|
- Sprint 38 (proc-macro 스캐폴딩): `cimery-macros` 크레이트 신설 (proc-macro=true, syn/quote/proc-macro2). `#[derive(ParamSummary)]` 구현 — struct named field 개수·이름 compile-time 상수 생성. 테스트 3개 (count/names/empty). ADR-002 D `#[param(unit,range,default)]` 전면 구현은 후속 스프린트.
|
||||||
|
- Sprint 39 (변단면 거더): `SceneParams.variable_depth_mm`(0~800mm) 추가. `apply_variable_depth()` — 경간 [z0,z1]에서 포물선 soffit lift 를 정점 Y 에 선형 보간 적용(`y_new = y + lift(u)·(1 - y/h)`). 거더 상면은 고정, 소핏이 중앙부에서 올라가 web 축소 → 연속교 관례 형상. camber 와 독립 조합 가능.
|
||||||
- code — Sprint 35: IFC 뷰어 통합 + Pset_BeamCommon.
|
- code — Sprint 35: IFC 뷰어 통합 + Pset_BeamCommon.
|
||||||
- `cimery-viewer` 에 `cimery-ifc` 의존성 추가. `project_file::scene_params_to_ifc()` 변환 함수 (SceneParams → BridgeExportParams 전 필드 매핑).
|
- `cimery-viewer` 에 `cimery-ifc` 의존성 추가. `project_file::scene_params_to_ifc()` 변환 함수 (SceneParams → BridgeExportParams 전 필드 매핑).
|
||||||
- 프로젝트 섹션에 "📤 IFC4X3 익스포트" 버튼. `projects/bridge.ifc` 로 저장, 현재 파라미터(경간 수·교각 형식·skew·헌치·단면 등) 그대로 반영.
|
- 프로젝트 섹션에 "📤 IFC4X3 익스포트" 버튼. `projects/bridge.ifc` 로 저장, 현재 파라미터(경간 수·교각 형식·skew·헌치·단면 등) 그대로 반영.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ members = [
|
|||||||
"crates/viewer",
|
"crates/viewer",
|
||||||
"crates/usd",
|
"crates/usd",
|
||||||
"crates/ifc",
|
"crates/ifc",
|
||||||
|
"crates/macros",
|
||||||
"crates/app",
|
"crates/app",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
@@ -30,8 +31,14 @@ cimery-incremental = { path = "crates/incremental" }
|
|||||||
cimery-evaluator = { path = "crates/evaluator" }
|
cimery-evaluator = { path = "crates/evaluator" }
|
||||||
cimery-usd = { path = "crates/usd" }
|
cimery-usd = { path = "crates/usd" }
|
||||||
cimery-ifc = { path = "crates/ifc" }
|
cimery-ifc = { path = "crates/ifc" }
|
||||||
|
cimery-macros = { path = "crates/macros" }
|
||||||
cimery-app = { path = "crates/app" }
|
cimery-app = { path = "crates/app" }
|
||||||
|
|
||||||
|
# proc-macro support (Sprint 38)
|
||||||
|
syn = { version = "2", features = ["full", "extra-traits"] }
|
||||||
|
quote = "1"
|
||||||
|
proc-macro2 = "1"
|
||||||
|
|
||||||
# Serialization
|
# Serialization
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|||||||
@@ -50,8 +50,15 @@ pub struct BridgeExportParams {
|
|||||||
pub haunch_depth: f64, // mm
|
pub haunch_depth: f64, // mm
|
||||||
pub show_parapets: bool,
|
pub show_parapets: bool,
|
||||||
pub show_joints: 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 {
|
impl Default for BridgeExportParams {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -68,6 +75,7 @@ impl Default for BridgeExportParams {
|
|||||||
haunch_depth: 0.0,
|
haunch_depth: 0.0,
|
||||||
show_parapets: true,
|
show_parapets: true,
|
||||||
show_joints: 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!(
|
w.write(&format!(
|
||||||
"IFCRELAGGREGATES({},$,$,$,{},({}))",
|
"IFCRELAGGREGATES({},$,$,$,{},({}))",
|
||||||
lit(&new_ifc_guid()),
|
lit(&new_ifc_guid()),
|
||||||
project,
|
project,
|
||||||
site,
|
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!(
|
w.write(&format!(
|
||||||
"IFCRELAGGREGATES({},$,$,$,{},({}))",
|
"IFCRELAGGREGATES({},$,$,$,{},({},{}))",
|
||||||
lit(&new_ifc_guid()),
|
lit(&new_ifc_guid()),
|
||||||
site,
|
site,
|
||||||
bridge,
|
bridge,
|
||||||
|
alignment,
|
||||||
));
|
));
|
||||||
|
|
||||||
// ── Bridge elements ───────────────────────────────────────────────────
|
// ── Bridge elements ───────────────────────────────────────────────────
|
||||||
@@ -137,37 +155,60 @@ pub fn export_bridge(p: &BridgeExportParams) -> String {
|
|||||||
let total_mm = span_mm * span_count as f64;
|
let total_mm = span_mm * span_count as f64;
|
||||||
|
|
||||||
let skew_rad = p.skew_deg.to_radians();
|
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 실제 단면.
|
// Girders (span_count × girder_count) — 거더 직선 유지(precast 관례).
|
||||||
// 거더는 직선 유지(precast 관례) → skew 미적용.
|
// Sprint 37: camber 있으면 CAMBER_SEGMENTS 분할 근사.
|
||||||
for s in 0..span_count {
|
for s in 0..span_count {
|
||||||
let z0 = span_mm * s as f64;
|
let z0 = span_mm * s as f64;
|
||||||
for i in 0..p.girder_count {
|
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 x = (i as f64 - (p.girder_count as f64 - 1.0) * 0.5) * p.girder_spacing;
|
||||||
let placement = write_local_placement(
|
let beam_center_y = p.bearing_height + p.girder_height * 0.5;
|
||||||
&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 girder_label = format!("Girder S{}-G{}", s + 1, i + 1);
|
let girder_label = format!("Girder S{}-G{}", s + 1, i + 1);
|
||||||
w.emit(
|
|
||||||
beam,
|
if !use_camber {
|
||||||
&format!(
|
// 직선 — 단일 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.)",
|
"IFCBEAM({},$,{},$,$,{},{},$,.BEAM.)",
|
||||||
lit(&new_ifc_guid()),
|
lit(&new_ifc_guid()), lit(&girder_label), placement, shape,
|
||||||
lit(&girder_label),
|
));
|
||||||
placement,
|
write_pset_beam_common(&mut w, beam, &girder_label, span_mm);
|
||||||
shape,
|
elements.push(beam);
|
||||||
),
|
} else {
|
||||||
);
|
// Camber — N 세그먼트로 포물선 Y 오프셋 근사.
|
||||||
// Pset_BeamCommon (Sprint 35).
|
let seg_len = span_mm / CAMBER_SEGMENTS as f64;
|
||||||
write_pset_beam_common(&mut w, beam, &girder_label, span_mm);
|
for k in 0..CAMBER_SEGMENTS {
|
||||||
elements.push(beam);
|
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).
|
/// Pset_BeamCommon 생성 + `IfcRelDefinesByProperties` 로 beam 에 연결 (Sprint 35 Phase 3a).
|
||||||
///
|
///
|
||||||
/// # 속성
|
/// # 속성
|
||||||
@@ -611,6 +742,45 @@ mod tests {
|
|||||||
assert_ne!(zero, skewed);
|
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]
|
#[test]
|
||||||
fn pset_beam_common_attached_to_girders() {
|
fn pset_beam_common_attached_to_girders() {
|
||||||
let ifc = export_bridge(&BridgeExportParams::default());
|
let ifc = export_bridge(&BridgeExportParams::default());
|
||||||
|
|||||||
13
cimery/crates/macros/Cargo.toml
Normal file
13
cimery/crates/macros/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "cimery-macros"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
description = "proc-macro helpers for cimery DSL (Sprint 38 scaffolding)."
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
syn = { workspace = true }
|
||||||
|
quote = { workspace = true }
|
||||||
|
proc-macro2 = { workspace = true }
|
||||||
82
cimery/crates/macros/src/lib.rs
Normal file
82
cimery/crates/macros/src/lib.rs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
//! cimery-macros — Feature DSL proc-macro 인프라 (Sprint 38).
|
||||||
|
//!
|
||||||
|
//! # 현재 범위
|
||||||
|
//! - `#[derive(ParamSummary)]` — 구조체의 필드 이름·개수·타입을 컴파일 타임에
|
||||||
|
//! 요약 제공. UI 자동생성·파라미터 인스펙터의 기반.
|
||||||
|
//!
|
||||||
|
//! # ADR-002 D 로드맵 (미래)
|
||||||
|
//! - `#[param(unit="mm", range=1000..=3000, default=1800)]` 속성 attribute
|
||||||
|
//! - `#[derive(Feature)]` — validation / builder / IFC 매핑 자동 생성
|
||||||
|
//! - 선언적 `feature!` 매크로 — DSL 설탕
|
||||||
|
//!
|
||||||
|
//! # 현 시점 사용 예
|
||||||
|
//! ```ignore
|
||||||
|
//! use cimery_macros::ParamSummary;
|
||||||
|
//!
|
||||||
|
//! #[derive(ParamSummary)]
|
||||||
|
//! pub struct GirderParams {
|
||||||
|
//! pub total_height: f64,
|
||||||
|
//! pub top_flange_width: f64,
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! fn main() {
|
||||||
|
//! assert_eq!(GirderParams::PARAM_COUNT, 2);
|
||||||
|
//! assert_eq!(GirderParams::PARAM_NAMES, &["total_height", "top_flange_width"]);
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::{parse_macro_input, Data, DeriveInput, Fields};
|
||||||
|
|
||||||
|
/// `#[derive(ParamSummary)]` — named field 구조체의 필드 이름·개수 상수 생성.
|
||||||
|
///
|
||||||
|
/// - `Self::PARAM_COUNT: usize`
|
||||||
|
/// - `Self::PARAM_NAMES: &'static [&'static str]`
|
||||||
|
///
|
||||||
|
/// # 제약
|
||||||
|
/// - named field 구조체만 지원 (tuple/unit/enum 은 컴파일 에러).
|
||||||
|
/// - 모든 필드가 노출됨 (private 필드 포함). 필터링은 Phase 2.
|
||||||
|
#[proc_macro_derive(ParamSummary)]
|
||||||
|
pub fn derive_param_summary(input: TokenStream) -> TokenStream {
|
||||||
|
let input = parse_macro_input!(input as DeriveInput);
|
||||||
|
let name = &input.ident;
|
||||||
|
|
||||||
|
let fields = match &input.data {
|
||||||
|
Data::Struct(s) => match &s.fields {
|
||||||
|
Fields::Named(named) => &named.named,
|
||||||
|
_ => {
|
||||||
|
return syn::Error::new_spanned(
|
||||||
|
name,
|
||||||
|
"ParamSummary only supports structs with named fields",
|
||||||
|
)
|
||||||
|
.to_compile_error()
|
||||||
|
.into();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
return syn::Error::new_spanned(
|
||||||
|
name,
|
||||||
|
"ParamSummary only supports structs, not enums or unions",
|
||||||
|
)
|
||||||
|
.to_compile_error()
|
||||||
|
.into();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let names: Vec<String> = fields
|
||||||
|
.iter()
|
||||||
|
.filter_map(|f| f.ident.as_ref().map(|i| i.to_string()))
|
||||||
|
.collect();
|
||||||
|
let count = names.len();
|
||||||
|
|
||||||
|
let expanded = quote! {
|
||||||
|
impl #name {
|
||||||
|
/// `#[derive(ParamSummary)]` 로 생성된 필드 개수.
|
||||||
|
pub const PARAM_COUNT: usize = #count;
|
||||||
|
/// `#[derive(ParamSummary)]` 로 생성된 필드 이름 목록 (선언 순서).
|
||||||
|
pub const PARAM_NAMES: &'static [&'static str] = &[ #( #names ),* ];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expanded.into()
|
||||||
|
}
|
||||||
34
cimery/crates/macros/tests/derive_test.rs
Normal file
34
cimery/crates/macros/tests/derive_test.rs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
//! `#[derive(ParamSummary)]` 기본 동작 검증.
|
||||||
|
|
||||||
|
use cimery_macros::ParamSummary;
|
||||||
|
|
||||||
|
#[derive(ParamSummary)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
struct GirderParams {
|
||||||
|
pub total_height: f64,
|
||||||
|
pub top_flange_width: f64,
|
||||||
|
pub web_thickness: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn param_count_matches_field_count() {
|
||||||
|
assert_eq!(GirderParams::PARAM_COUNT, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn param_names_in_declaration_order() {
|
||||||
|
assert_eq!(
|
||||||
|
GirderParams::PARAM_NAMES,
|
||||||
|
&["total_height", "top_flange_width", "web_thickness"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(ParamSummary)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
struct Empty {}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_struct_handled() {
|
||||||
|
assert_eq!(Empty::PARAM_COUNT, 0);
|
||||||
|
assert_eq!(Empty::PARAM_NAMES, &[] as &[&str]);
|
||||||
|
}
|
||||||
@@ -65,6 +65,10 @@ pub struct SceneParams {
|
|||||||
/// 데크 헌치 (Haunch) 깊이 [mm]. Sprint 31.
|
/// 데크 헌치 (Haunch) 깊이 [mm]. Sprint 31.
|
||||||
/// 거더 상부와 데크 soffit 사이 전환부 두께. 0 = 헌치 없음(데크 = 거더 상부 직접 접촉).
|
/// 거더 상부와 데크 soffit 사이 전환부 두께. 0 = 헌치 없음(데크 = 거더 상부 직접 접촉).
|
||||||
pub haunch_depth: f32,
|
pub haunch_depth: f32,
|
||||||
|
/// 변단면 거더 (Variable Depth, Sprint 39).
|
||||||
|
/// 연속교의 경우 지점부가 높고 경간 중앙이 낮은 포물선 변화.
|
||||||
|
/// 값 = 지점 대비 중앙부 단면 높이 **감소량** [mm]. 0 = 상수 단면.
|
||||||
|
pub variable_depth_mm: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SceneParams {
|
impl Default for SceneParams {
|
||||||
@@ -86,6 +90,7 @@ impl Default for SceneParams {
|
|||||||
show_diaphragms: true,
|
show_diaphragms: true,
|
||||||
camber_mid_mm: 0.0,
|
camber_mid_mm: 0.0,
|
||||||
haunch_depth: 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
|
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 오프셋.
|
/// Sprint 30: Camber(솟음) 포물선 Y 오프셋.
|
||||||
/// 경간 [z0, z1] 내에서 중앙에서 `mid_mm` 만큼 위로 솟음.
|
/// 경간 [z0, z1] 내에서 중앙에서 `mid_mm` 만큼 위로 솟음.
|
||||||
/// z - z0 = u ∈ [0, span]. y_off = 4 · mid · u · (span - u) / span² (중앙 u=span/2 에서 최대 = mid)
|
/// 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)?;
|
let mut mesh = kernel.girder_mesh(&ir)?;
|
||||||
mesh.recolor(COL_GIRDER);
|
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);
|
let mut placed = translate(mesh, x, 0.0, z_base);
|
||||||
apply_camber_mesh(&mut placed, z_base, z_end, p.camber_mid_mm);
|
apply_camber_mesh(&mut placed, z_base, z_end, p.camber_mid_mm);
|
||||||
parts.push(placed);
|
parts.push(placed);
|
||||||
@@ -607,6 +637,10 @@ pub fn build_selectable_scene<K: GeomKernel>(
|
|||||||
};
|
};
|
||||||
let mut mesh = kernel.girder_mesh(&ir)?;
|
let mut mesh = kernel.girder_mesh(&ir)?;
|
||||||
mesh.recolor(COL_GIRDER);
|
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; }
|
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);
|
apply_camber_mesh(&mut mesh, z_base, z_end, p.camber_mid_mm);
|
||||||
let label = if span_count > 1 {
|
let label = if span_count > 1 {
|
||||||
|
|||||||
@@ -656,6 +656,8 @@ impl RenderState {
|
|||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ps!(ui, "경사각 (°)", &mut p.skew_deg, -30.0..=30.0, 1.0);
|
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);
|
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) ───────────────────────────
|
// ── 하부구조 (Substructure) ───────────────────────────
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ pub struct ProjectFile {
|
|||||||
/// Sprint 31: 데크 헌치(Haunch) 깊이 [mm]
|
/// Sprint 31: 데크 헌치(Haunch) 깊이 [mm]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub haunch_depth: f32,
|
pub haunch_depth: f32,
|
||||||
|
/// Sprint 39: 변단면 거더 중앙부 높이 감소 [mm]
|
||||||
|
#[serde(default)]
|
||||||
|
pub variable_depth_mm: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_true() -> bool { true }
|
fn default_true() -> bool { true }
|
||||||
@@ -81,6 +84,7 @@ impl ProjectFile {
|
|||||||
show_diaphragms: p.show_diaphragms,
|
show_diaphragms: p.show_diaphragms,
|
||||||
camber_mid_mm: p.camber_mid_mm,
|
camber_mid_mm: p.camber_mid_mm,
|
||||||
haunch_depth: p.haunch_depth,
|
haunch_depth: p.haunch_depth,
|
||||||
|
variable_depth_mm: p.variable_depth_mm,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +112,7 @@ impl ProjectFile {
|
|||||||
show_diaphragms: self.show_diaphragms,
|
show_diaphragms: self.show_diaphragms,
|
||||||
camber_mid_mm: self.camber_mid_mm,
|
camber_mid_mm: self.camber_mid_mm,
|
||||||
haunch_depth: self.haunch_depth,
|
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,
|
haunch_depth: p.haunch_depth as f64,
|
||||||
show_parapets: true,
|
show_parapets: true,
|
||||||
show_joints: p.show_expansion_joints,
|
show_joints: p.show_expansion_joints,
|
||||||
|
camber_mid_mm: p.camber_mid_mm as f64,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user