Sprint 31/32 — 헌치 + 속성 패널 카테고리 재정리
Sprint 31: Haunch (데크 헌치). - SceneParams.haunch_depth (0~300mm, step 10mm). - 거더 상부와 데크 soffit 사이 600mm×haunch_d×span 블록을 경간별·거더별 자동 배치. COL_DECK 로 색상 통일. - 데크 위치: girder_h + slab_thickness → girder_h + haunch_depth + slab_thickness. 6군데(build_bridge_scene + build_selectable_scene 의 데크·신축이음·방호벽) 일괄 수정. - camber + skew 동시 적용. - UI: "헌치 (mm)" 슬라이더. Sprint 32: 속성 패널 재정리. - 누적 11개 슬라이더가 한 섹션에 섞여 혼잡 → 5개 CollapsingHeader 분리: · 상부구조 (경간·거더 관련 5항목) · 바닥판 (슬래브·헌치) · 선형·기하 (경사각·솟음) · 하부구조 (교각 형식) · 추가 부재 (가로보·신축이음·격벽 — 기존 유지) · 표시 (선형·투영 — 기존 유지) - ps!($ui, ...) 매크로 hygiene 수정: ui 명시적 매개변수화로 macro_rules 기본 hygiene 의 외부 캡처 문제 회피. - "경간" 라벨 중복(span_m vs span_count) 해소: "경간 길이"/"경간 수". ProjectFile: haunch_depth 필드 추가 (default 0.0). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,10 @@
|
|||||||
## 타임라인
|
## 타임라인
|
||||||
|
|
||||||
### 2026-04-15 (계속)
|
### 2026-04-15 (계속)
|
||||||
|
- code — Sprint 31~32: 헌치 + UI 재정리.
|
||||||
|
- Sprint 31: 데크 헌치 (Haunch). `SceneParams.haunch_depth` (0~300mm) 추가. 거더 상부와 데크 soffit 사이 600mm 폭 × haunch_d 높이 블록을 거더마다 배치. 데크 위치는 `girder_h + haunch_depth + slab_thickness` 로 이동 (기존 6개 참조 일괄 수정). camber + skew 동시 적용.
|
||||||
|
- Sprint 32: 속성 패널 카테고리 재정리 (누적 11개 슬라이더 섞여 혼잡). 5개 CollapsingHeader 로 분리: 상부구조·바닥판·선형/기하·하부구조·추가부재·표시. `ps!($ui, ...)` 매크로 hygiene 수정(ui 명시적 매개변수화).
|
||||||
|
- ProjectFile: haunch_depth 필드.
|
||||||
- code — Sprint 29~30: 거더교 MVP 추가 확장.
|
- code — Sprint 29~30: 거더교 MVP 추가 확장.
|
||||||
- Sprint 29: 지점부 격벽 (Diaphragm). `SceneParams.show_diaphragms` 토글(default true). 모든 지점(교대·교각) 에서 인접 거더 사이 RC 벽 자동 배치. 두께 300mm(span 방향), 높이 = girder_h, 폭 = spacing - 250mm(web clearance). skew 회전 동시 적용. `build_bridge_scene` + `build_selectable_scene` 양쪽.
|
- Sprint 29: 지점부 격벽 (Diaphragm). `SceneParams.show_diaphragms` 토글(default true). 모든 지점(교대·교각) 에서 인접 거더 사이 RC 벽 자동 배치. 두께 300mm(span 방향), 높이 = girder_h, 폭 = spacing - 250mm(web clearance). skew 회전 동시 적용. `build_bridge_scene` + `build_selectable_scene` 양쪽.
|
||||||
- Sprint 30: 솟음 (Camber). `SceneParams.camber_mid_mm`(0~200mm) 추가. `apply_camber_mesh()` 헬퍼 — 경간 [z0, z1] 내 포물선 Y 오프셋 `4·mid·u·(span-u)/span²`. 거더·데크에 경간마다 독립 적용. 지점에서는 0. UI "솟음(mm)" 슬라이더.
|
- Sprint 30: 솟음 (Camber). `SceneParams.camber_mid_mm`(0~200mm) 추가. `apply_camber_mesh()` 헬퍼 — 경간 [z0, z1] 내 포물선 Y 오프셋 `4·mid·u·(span-u)/span²`. 거더·데크에 경간마다 독립 적용. 지점에서는 0. UI "솟음(mm)" 슬라이더.
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ pub struct SceneParams {
|
|||||||
/// 거더 솟음 (Camber) 중앙 솟음량 [mm]. Sprint 30.
|
/// 거더 솟음 (Camber) 중앙 솟음량 [mm]. Sprint 30.
|
||||||
/// 거더·데크에 경간 중앙 기준 포물선 Y 오프셋 적용. 0 = 평탄.
|
/// 거더·데크에 경간 중앙 기준 포물선 Y 오프셋 적용. 0 = 평탄.
|
||||||
pub camber_mid_mm: f32,
|
pub camber_mid_mm: f32,
|
||||||
|
/// 데크 헌치 (Haunch) 깊이 [mm]. Sprint 31.
|
||||||
|
/// 거더 상부와 데크 soffit 사이 전환부 두께. 0 = 헌치 없음(데크 = 거더 상부 직접 접촉).
|
||||||
|
pub haunch_depth: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SceneParams {
|
impl Default for SceneParams {
|
||||||
@@ -82,6 +85,7 @@ impl Default for SceneParams {
|
|||||||
show_expansion_joints: true,
|
show_expansion_joints: true,
|
||||||
show_diaphragms: true,
|
show_diaphragms: true,
|
||||||
camber_mid_mm: 0.0,
|
camber_mid_mm: 0.0,
|
||||||
|
haunch_depth: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,13 +282,35 @@ pub fn build_bridge_scene<K: GeomKernel>(kernel: &K, p: &SceneParams) -> Result<
|
|||||||
};
|
};
|
||||||
let mut deck_mesh = kernel.deck_slab_mesh(&deck_ir)?;
|
let mut deck_mesh = kernel.deck_slab_mesh(&deck_ir)?;
|
||||||
deck_mesh.recolor(COL_DECK);
|
deck_mesh.recolor(COL_DECK);
|
||||||
let mut deck_placed = translate(deck_mesh, 0.0, girder_h + p.slab_thickness, 0.0);
|
let mut deck_placed = translate(deck_mesh, 0.0, girder_h + p.haunch_depth + p.slab_thickness, 0.0);
|
||||||
for s in 0..span_count {
|
for s in 0..span_count {
|
||||||
let z0 = span_mm * s as f32;
|
let z0 = span_mm * s as f32;
|
||||||
apply_camber_mesh(&mut deck_placed, z0, z0 + span_mm, p.camber_mid_mm);
|
apply_camber_mesh(&mut deck_placed, z0, z0 + span_mm, p.camber_mid_mm);
|
||||||
}
|
}
|
||||||
parts.push(deck_placed);
|
parts.push(deck_placed);
|
||||||
|
|
||||||
|
// ── Haunch (Sprint 31: 거더 상부와 데크 soffit 사이 전환부) ──────────────
|
||||||
|
if p.haunch_depth > 0.1 {
|
||||||
|
const HAUNCH_W: f32 = 600.0; // PSC-I top flange width 기준
|
||||||
|
for s in 0..span_count {
|
||||||
|
let z_base = span_mm * s as f32;
|
||||||
|
for i in 0..n_girders {
|
||||||
|
let x = (i as f32 - (n_girders as f32 - 1.0) * 0.5) * spacing;
|
||||||
|
let profile = vec![
|
||||||
|
[-HAUNCH_W * 0.5, 0.0],
|
||||||
|
[ HAUNCH_W * 0.5, 0.0],
|
||||||
|
[ HAUNCH_W * 0.5, p.haunch_depth],
|
||||||
|
[-HAUNCH_W * 0.5, p.haunch_depth],
|
||||||
|
];
|
||||||
|
let mut mesh = cimery_kernel::sweep::sweep_profile_flat(&profile, span_mm);
|
||||||
|
mesh.recolor(COL_DECK);
|
||||||
|
for v in &mut mesh.vertices { v[0] += x; v[1] += girder_h; v[2] += z_base; }
|
||||||
|
apply_camber_mesh(&mut mesh, z_base, z_base + span_mm, p.camber_mid_mm);
|
||||||
|
parts.push(mesh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Sprint 27: Skew rad (교대·교각·받침·신축이음에 적용. 거더·데크는 직선 유지).
|
// Sprint 27: Skew rad (교대·교각·받침·신축이음에 적용. 거더·데크는 직선 유지).
|
||||||
let skew_rad = p.skew_deg.to_radians();
|
let skew_rad = p.skew_deg.to_radians();
|
||||||
|
|
||||||
@@ -408,7 +434,7 @@ pub fn build_bridge_scene<K: GeomKernel>(kernel: &K, p: &SceneParams) -> Result<
|
|||||||
// ── Expansion Joints (양 교대 + 내부 피어 위치) ──────────────────────────
|
// ── Expansion Joints (양 교대 + 내부 피어 위치) ──────────────────────────
|
||||||
if p.show_expansion_joints {
|
if p.show_expansion_joints {
|
||||||
let deck_w = ((n_girders as f32 - 1.0) * spacing + 2_000.0) as f64;
|
let deck_w = ((n_girders as f32 - 1.0) * spacing + 2_000.0) as f64;
|
||||||
let y_top = girder_h + p.slab_thickness;
|
let y_top = girder_h + p.haunch_depth + p.slab_thickness;
|
||||||
for &z in &support_zs {
|
for &z in &support_zs {
|
||||||
let ej_ir = ExpansionJointIR {
|
let ej_ir = ExpansionJointIR {
|
||||||
id: FeatureId::new(),
|
id: FeatureId::new(),
|
||||||
@@ -431,7 +457,7 @@ pub fn build_bridge_scene<K: GeomKernel>(kernel: &K, p: &SceneParams) -> Result<
|
|||||||
{
|
{
|
||||||
const PARAPET_H: f32 = 1_200.0;
|
const PARAPET_H: f32 = 1_200.0;
|
||||||
const PARAPET_T: f32 = 500.0;
|
const PARAPET_T: f32 = 500.0;
|
||||||
let y_base = girder_h + p.slab_thickness;
|
let y_base = girder_h + p.haunch_depth + p.slab_thickness;
|
||||||
let x_outer = half_width;
|
let x_outer = half_width;
|
||||||
for &x_center in &[x_outer - PARAPET_T * 0.5, -x_outer + PARAPET_T * 0.5] {
|
for &x_center in &[x_outer - PARAPET_T * 0.5, -x_outer + PARAPET_T * 0.5] {
|
||||||
let profile = vec![
|
let profile = vec![
|
||||||
@@ -602,13 +628,40 @@ pub fn build_selectable_scene<K: GeomKernel>(
|
|||||||
};
|
};
|
||||||
let mut deck = kernel.deck_slab_mesh(&deck_ir)?;
|
let mut deck = kernel.deck_slab_mesh(&deck_ir)?;
|
||||||
deck.recolor(COL_DECK);
|
deck.recolor(COL_DECK);
|
||||||
for v in &mut deck.vertices { v[1] += girder_h + p.slab_thickness; }
|
for v in &mut deck.vertices { v[1] += girder_h + p.haunch_depth + p.slab_thickness; }
|
||||||
for s in 0..span_count {
|
for s in 0..span_count {
|
||||||
let z0 = span_mm * s as f32;
|
let z0 = span_mm * s as f32;
|
||||||
apply_camber_mesh(&mut deck, z0, z0 + span_mm, p.camber_mid_mm);
|
apply_camber_mesh(&mut deck, z0, z0 + span_mm, p.camber_mid_mm);
|
||||||
}
|
}
|
||||||
out.push(FeatureMesh { mesh: deck, label: "바닥판 슬래브".into() });
|
out.push(FeatureMesh { mesh: deck, label: "바닥판 슬래브".into() });
|
||||||
|
|
||||||
|
// Haunch (Sprint 31)
|
||||||
|
if p.haunch_depth > 0.1 {
|
||||||
|
const HAUNCH_W: f32 = 600.0;
|
||||||
|
for s in 0..span_count {
|
||||||
|
let z_base = span_mm * s as f32;
|
||||||
|
for i in 0..n_girders {
|
||||||
|
let x = (i as f32 - (n_girders as f32 - 1.0) * 0.5) * spacing;
|
||||||
|
let profile = vec![
|
||||||
|
[-HAUNCH_W * 0.5, 0.0],
|
||||||
|
[ HAUNCH_W * 0.5, 0.0],
|
||||||
|
[ HAUNCH_W * 0.5, p.haunch_depth],
|
||||||
|
[-HAUNCH_W * 0.5, p.haunch_depth],
|
||||||
|
];
|
||||||
|
let mut mesh = cimery_kernel::sweep::sweep_profile_flat(&profile, span_mm);
|
||||||
|
mesh.recolor(COL_DECK);
|
||||||
|
for v in &mut mesh.vertices { v[0] += x; v[1] += girder_h; v[2] += z_base; }
|
||||||
|
apply_camber_mesh(&mut mesh, z_base, z_base + span_mm, p.camber_mid_mm);
|
||||||
|
let label = if span_count > 1 {
|
||||||
|
format!("헌치 {}-{}", s + 1, i + 1)
|
||||||
|
} else {
|
||||||
|
format!("헌치 {}", i + 1)
|
||||||
|
};
|
||||||
|
out.push(FeatureMesh { mesh, label });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Sprint 27: skew rad — 교대·교각·받침·신축이음에 적용.
|
// Sprint 27: skew rad — 교대·교각·받침·신축이음에 적용.
|
||||||
let skew_rad = p.skew_deg.to_radians();
|
let skew_rad = p.skew_deg.to_radians();
|
||||||
|
|
||||||
@@ -718,7 +771,7 @@ pub fn build_selectable_scene<K: GeomKernel>(
|
|||||||
};
|
};
|
||||||
let mut mesh = kernel.expansion_joint_mesh(&ej_ir)?;
|
let mut mesh = kernel.expansion_joint_mesh(&ej_ir)?;
|
||||||
mesh.recolor(COL_EXP_JOINT);
|
mesh.recolor(COL_EXP_JOINT);
|
||||||
let y_top = girder_h + p.slab_thickness;
|
let y_top = girder_h + p.haunch_depth + p.slab_thickness;
|
||||||
for v in &mut mesh.vertices {
|
for v in &mut mesh.vertices {
|
||||||
v[1] += y_top;
|
v[1] += y_top;
|
||||||
v[2] += z;
|
v[2] += z;
|
||||||
@@ -736,7 +789,7 @@ pub fn build_selectable_scene<K: GeomKernel>(
|
|||||||
{
|
{
|
||||||
const PARAPET_H: f32 = 1_200.0;
|
const PARAPET_H: f32 = 1_200.0;
|
||||||
const PARAPET_T: f32 = 500.0;
|
const PARAPET_T: f32 = 500.0;
|
||||||
let y_base = girder_h + p.slab_thickness;
|
let y_base = girder_h + p.haunch_depth + p.slab_thickness;
|
||||||
let x_outer = half_w;
|
let x_outer = half_w;
|
||||||
for &(x_center, side_label) in &[(x_outer - PARAPET_T * 0.5, "우"), (-x_outer + PARAPET_T * 0.5, "좌")] {
|
for &(x_center, side_label) in &[(x_outer - PARAPET_T * 0.5, "우"), (-x_outer + PARAPET_T * 0.5, "좌")] {
|
||||||
let profile = vec![
|
let profile = vec![
|
||||||
|
|||||||
@@ -607,36 +607,26 @@ impl RenderState {
|
|||||||
ui.heading("속성 패널");
|
ui.heading("속성 패널");
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
// ── 상부구조 (Superstructure) ──────────────────────────
|
// Sprint 32: 속성 패널 카테고리 재정리.
|
||||||
egui::CollapsingHeader::new("▼ 상부구조 (Superstructure)")
|
// ps!($ui, $label, $value, $range, $step) — $ui 를 명시해서 매크로 hygiene 회피.
|
||||||
.default_open(true)
|
|
||||||
.show(ui, |ui| {
|
|
||||||
macro_rules! ps {
|
macro_rules! ps {
|
||||||
($lbl:expr, $v:expr, $r:expr, $s:expr) => {{
|
($ui:expr, $lbl:expr, $v:expr, $r:expr, $s:expr) => {{
|
||||||
ui.label($lbl);
|
$ui.label($lbl);
|
||||||
if ui.add(egui::Slider::new($v, $r).step_by($s)).changed() {
|
if $ui.add(egui::Slider::new($v, $r).step_by($s)).changed() {
|
||||||
dirty = true;
|
dirty = true;
|
||||||
}
|
}
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
ps!("경간 (m)", &mut p.span_m, 20.0..=80.0, 1.0);
|
|
||||||
ps!("거더 수", &mut p.girder_count, 3..=7, 1.0);
|
// ── 상부구조 (Superstructure) ──────────────────────────
|
||||||
ps!("c/c 간격 (mm)", &mut p.girder_spacing, 1_500.0..=4_000.0, 100.0);
|
egui::CollapsingHeader::new("▼ 상부구조 (Superstructure)")
|
||||||
ps!("거더 높이 (mm)", &mut p.girder_height, 1_000.0..=3_000.0, 100.0);
|
.default_open(true)
|
||||||
ps!("슬래브 두께 (mm)",&mut p.slab_thickness, 150.0..=400.0, 10.0);
|
.show(ui, |ui| {
|
||||||
// Sprint 26: 다경간 지원
|
ps!(ui, "경간 길이 (m)", &mut p.span_m, 20.0..=80.0, 1.0);
|
||||||
ps!("경간 수", &mut p.span_count, 1..=5, 1.0);
|
ps!(ui, "경간 수", &mut p.span_count, 1..=5, 1.0);
|
||||||
ui.label("교각 형식");
|
ps!(ui, "거더 수", &mut p.girder_count, 3..=7, 1.0);
|
||||||
let prev_pt = p.pier_type;
|
ps!(ui, "c/c 간격 (mm)", &mut p.girder_spacing, 1_500.0..=4_000.0, 100.0);
|
||||||
ui.horizontal(|ui| {
|
ps!(ui, "거더 높이 (mm)", &mut p.girder_height, 1_000.0..=3_000.0, 100.0);
|
||||||
ui.selectable_value(&mut p.pier_type, cimery_core::PierType::SingleColumn, "T형(단주)");
|
|
||||||
ui.selectable_value(&mut p.pier_type, cimery_core::PierType::MultiColumn, "π형(다주)");
|
|
||||||
});
|
|
||||||
if p.pier_type != prev_pt { dirty = true; }
|
|
||||||
// Sprint 27: 경사각 (Skew)
|
|
||||||
ps!("경사각 (°)", &mut p.skew_deg, -30.0..=30.0, 1.0);
|
|
||||||
// Sprint 30: 솟음 (Camber) 중앙 솟음량 [mm]
|
|
||||||
ps!("솟음 (mm)", &mut p.camber_mid_mm, 0.0..=200.0, 5.0);
|
|
||||||
|
|
||||||
ui.label("단면 형식");
|
ui.label("단면 형식");
|
||||||
let prev_sec = p.section_type;
|
let prev_sec = p.section_type;
|
||||||
@@ -652,6 +642,35 @@ impl RenderState {
|
|||||||
if p.section_type != prev_sec { dirty = true; }
|
if p.section_type != prev_sec { dirty = true; }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── 바닥판 (Deck) ─────────────────────────────────────
|
||||||
|
egui::CollapsingHeader::new("▼ 바닥판 (Deck)")
|
||||||
|
.default_open(true)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ps!(ui, "슬래브 두께 (mm)",&mut p.slab_thickness, 150.0..=400.0, 10.0);
|
||||||
|
ps!(ui, "헌치 (mm)", &mut p.haunch_depth, 0.0..=300.0, 10.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── 선형·기하 (Geometry) ──────────────────────────────
|
||||||
|
egui::CollapsingHeader::new("▼ 선형·기하 (Geometry)")
|
||||||
|
.default_open(true)
|
||||||
|
.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);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── 하부구조 (Substructure) ───────────────────────────
|
||||||
|
egui::CollapsingHeader::new("▼ 하부구조 (Substructure)")
|
||||||
|
.default_open(true)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.label("교각 형식");
|
||||||
|
let prev_pt = p.pier_type;
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.selectable_value(&mut p.pier_type, cimery_core::PierType::SingleColumn, "T형(단주)");
|
||||||
|
ui.selectable_value(&mut p.pier_type, cimery_core::PierType::MultiColumn, "π형(다주)");
|
||||||
|
});
|
||||||
|
if p.pier_type != prev_pt { dirty = true; }
|
||||||
|
});
|
||||||
|
|
||||||
// ── Should Features (Sprint 19) ────────────────────────
|
// ── Should Features (Sprint 19) ────────────────────────
|
||||||
egui::CollapsingHeader::new("▼ 추가 부재 (Should Features)")
|
egui::CollapsingHeader::new("▼ 추가 부재 (Should Features)")
|
||||||
.default_open(true)
|
.default_open(true)
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ pub struct ProjectFile {
|
|||||||
/// Sprint 30: 솟음(Camber) 중앙값 [mm]
|
/// Sprint 30: 솟음(Camber) 중앙값 [mm]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub camber_mid_mm: f32,
|
pub camber_mid_mm: f32,
|
||||||
|
/// Sprint 31: 데크 헌치(Haunch) 깊이 [mm]
|
||||||
|
#[serde(default)]
|
||||||
|
pub haunch_depth: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_true() -> bool { true }
|
fn default_true() -> bool { true }
|
||||||
@@ -77,6 +80,7 @@ impl ProjectFile {
|
|||||||
skew_deg: p.skew_deg,
|
skew_deg: p.skew_deg,
|
||||||
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +107,7 @@ impl ProjectFile {
|
|||||||
skew_deg: self.skew_deg,
|
skew_deg: self.skew_deg,
|
||||||
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user