Sprint 29/30 — 지점부 격벽 + 거더 솟음
Sprint 29: Diaphragm (지점부 격벽). - SceneParams.show_diaphragms 토글 (default true). - 모든 지점(교대+교각) 에서 인접 거더 사이 RC 벽 배치: · 두께(span 방향): 300mm · 높이: girder_h (거더 soffit ~ top) · 폭: spacing - 250mm (web clearance 양쪽 125mm) · 지점 Z 기준 중앙 배치, skew 회전 동시 적용 - build_bridge_scene + build_selectable_scene 양쪽 구현. - COL_DIAPHRAGM 색상 추가 (concrete 계열). - UI: "격벽 (Diaphragm)" 체크박스. Sprint 30: Camber (거더 솟음). - SceneParams.camber_mid_mm (0~200mm, step 5mm) 추가. - apply_camber_mesh(mesh, z0, z1, mid_mm) 헬퍼: u = z - z0 ∈ [0, span], y_off = 4·mid·u·(span-u)/span² (포물선). 지점(u=0 또는 u=span) 에서는 0, 중앙 u=span/2 에서 최대 mid. - 거더·데크에 경간마다 독립 적용. 다경간 교량도 경간별 정확한 solog. - UI: "솟음 (mm)" 슬라이더. ProjectFile: show_diaphragms + camber_mid_mm 필드 (default 값으로 v2 호환). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -57,6 +57,11 @@ pub struct SceneParams {
|
||||
pub cross_beam_interval_m: f64,
|
||||
/// Show expansion joints at span ends. Sprint 19.
|
||||
pub show_expansion_joints: bool,
|
||||
/// 지점부 격벽 표시 (Sprint 29). 모든 지점(교대+교각) 에서 거더 사이 RC 벽.
|
||||
pub show_diaphragms: bool,
|
||||
/// 거더 솟음 (Camber) 중앙 솟음량 [mm]. Sprint 30.
|
||||
/// 거더·데크에 경간 중앙 기준 포물선 Y 오프셋 적용. 0 = 평탄.
|
||||
pub camber_mid_mm: f32,
|
||||
}
|
||||
|
||||
impl Default for SceneParams {
|
||||
@@ -75,6 +80,8 @@ impl Default for SceneParams {
|
||||
show_cross_beams: true,
|
||||
cross_beam_interval_m: 5.0,
|
||||
show_expansion_joints: true,
|
||||
show_diaphragms: true,
|
||||
camber_mid_mm: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,6 +96,7 @@ pub const COL_ALIGNMENT: [f32; 3] = [1.00, 0.60, 0.10]; // orange centreline
|
||||
pub const COL_CROSS_BEAM: [f32; 3] = [0.75, 0.73, 0.65]; // slightly lighter concrete
|
||||
pub const COL_EXP_JOINT: [f32; 3] = [0.20, 0.20, 0.25]; // dark steel
|
||||
pub const COL_PIER: [f32; 3] = [0.68, 0.64, 0.55]; // pier concrete (Sprint 26)
|
||||
pub const COL_DIAPHRAGM: [f32; 3] = [0.70, 0.67, 0.58]; // diaphragm RC (Sprint 29)
|
||||
|
||||
// ─── Pier helper (Sprint 26) ─────────────────────────────────────────────────
|
||||
|
||||
@@ -142,6 +150,22 @@ fn translate(mut mesh: Mesh, dx: f32, dy: f32, dz: f32) -> Mesh {
|
||||
mesh
|
||||
}
|
||||
|
||||
/// 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)
|
||||
/// 경간 밖(z < z0 또는 z > z1)은 0. 거더·데크 mesh 사후 적용.
|
||||
fn apply_camber_mesh(mesh: &mut Mesh, z0: f32, z1: f32, mid_mm: f32) {
|
||||
if mid_mm.abs() < 1e-3 { 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 {
|
||||
v[1] += 4.0 * mid_mm * u * (span - u) / (span * span);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sprint 27: Y 축 중심 회전 (skew). pivot (X, Y 는 무시, Z 만 사용) 기준 각도 [rad].
|
||||
/// 정점·법선 모두 회전. 교대·교각·받침·신축이음에 skew 각 적용 시 사용.
|
||||
fn rotate_y_around_z(mut mesh: Mesh, angle_rad: f32, pivot_z: f32) -> Mesh {
|
||||
@@ -213,9 +237,10 @@ pub fn build_bridge_scene<K: GeomKernel>(kernel: &K, p: &SceneParams) -> Result<
|
||||
GirderSectionType::SteelBox => SectionType::SteelBox,
|
||||
};
|
||||
|
||||
// ── Girders (경간마다 독립 세트) ───────────────────────────────────────────
|
||||
// ── Girders (경간마다 독립 세트, camber 적용) ─────────────────────────────
|
||||
for s in 0..span_count {
|
||||
let z_base = span_mm * s as f32;
|
||||
let z_base = span_mm * s as f32;
|
||||
let z_end = z_base + span_mm;
|
||||
let s_start = span_m * s as f64;
|
||||
for i in 0..n_girders {
|
||||
let x = (i as f32 - (n_girders as f32 - 1.0) * 0.5) * spacing;
|
||||
@@ -232,11 +257,13 @@ pub fn build_bridge_scene<K: GeomKernel>(kernel: &K, p: &SceneParams) -> Result<
|
||||
};
|
||||
let mut mesh = kernel.girder_mesh(&ir)?;
|
||||
mesh.recolor(COL_GIRDER);
|
||||
parts.push(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);
|
||||
parts.push(placed);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Deck Slab (전 구간 연속) ───────────────────────────────────────────────
|
||||
// ── Deck Slab (전 구간 연속, 경간별 camber) ──────────────────────────────
|
||||
let half_width = ((n_girders as f32 - 1.0) * spacing) * 0.5 + 1_000.0;
|
||||
let deck_ir = DeckSlabIR {
|
||||
id: FeatureId::new(),
|
||||
@@ -251,7 +278,12 @@ pub fn build_bridge_scene<K: GeomKernel>(kernel: &K, p: &SceneParams) -> Result<
|
||||
};
|
||||
let mut deck_mesh = kernel.deck_slab_mesh(&deck_ir)?;
|
||||
deck_mesh.recolor(COL_DECK);
|
||||
parts.push(translate(deck_mesh, 0.0, girder_h + p.slab_thickness, 0.0));
|
||||
let mut deck_placed = translate(deck_mesh, 0.0, girder_h + p.slab_thickness, 0.0);
|
||||
for s in 0..span_count {
|
||||
let z0 = span_mm * s as f32;
|
||||
apply_camber_mesh(&mut deck_placed, z0, z0 + span_mm, p.camber_mid_mm);
|
||||
}
|
||||
parts.push(deck_placed);
|
||||
|
||||
// Sprint 27: Skew rad (교대·교각·받침·신축이음에 적용. 거더·데크는 직선 유지).
|
||||
let skew_rad = p.skew_deg.to_radians();
|
||||
@@ -415,6 +447,30 @@ pub fn build_bridge_scene<K: GeomKernel>(kernel: &K, p: &SceneParams) -> Result<
|
||||
}
|
||||
}
|
||||
|
||||
// ── Diaphragms (Sprint 29: 지점부 격벽) ───────────────────────────────────
|
||||
// 모든 지점(교대+교각) 에서 인접 거더 사이 RC 벽.
|
||||
// 높이 = girder_h, 두께(span 방향) = 300mm, 폭 = spacing - web_thickness 여유.
|
||||
if p.show_diaphragms {
|
||||
const DIA_THICK: f32 = 300.0; // span 방향 두께
|
||||
const WEB_CLEAR: f32 = 250.0; // 거더 web 양쪽 총 여유 (125mm×2)
|
||||
let dia_w = spacing - WEB_CLEAR;
|
||||
for &z in &support_zs {
|
||||
for i in 0..n_girders.saturating_sub(1) {
|
||||
let x_mid = ((i as f32) - (n_girders as f32 - 1.0) * 0.5 + 0.5) * spacing;
|
||||
let profile = vec![
|
||||
[x_mid - dia_w * 0.5, 0.0],
|
||||
[x_mid + dia_w * 0.5, 0.0],
|
||||
[x_mid + dia_w * 0.5, girder_h],
|
||||
[x_mid - dia_w * 0.5, girder_h],
|
||||
];
|
||||
let mut mesh = cimery_kernel::sweep::sweep_profile_flat(&profile, DIA_THICK);
|
||||
mesh.recolor(COL_DIAPHRAGM);
|
||||
let placed = translate(mesh, 0.0, 0.0, z - DIA_THICK * 0.5);
|
||||
parts.push(rotate_y_around_z(placed, skew_rad, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(merge(parts))
|
||||
}
|
||||
|
||||
@@ -510,9 +566,10 @@ pub fn build_selectable_scene<K: GeomKernel>(
|
||||
|
||||
let mut out: Vec<FeatureMesh> = Vec::new();
|
||||
|
||||
// Girders (경간마다 독립 세트)
|
||||
// Girders (경간마다 독립 세트, camber 적용)
|
||||
for s in 0..span_count {
|
||||
let z_base = span_mm * s as f32;
|
||||
let z_end = z_base + span_mm;
|
||||
let s_start = span_m * s as f64;
|
||||
for i in 0..n_girders {
|
||||
let x = (i as f32 - (n_girders as f32 - 1.0) * 0.5) * spacing;
|
||||
@@ -525,6 +582,7 @@ pub fn build_selectable_scene<K: GeomKernel>(
|
||||
let mut mesh = kernel.girder_mesh(&ir)?;
|
||||
mesh.recolor(COL_GIRDER);
|
||||
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 {
|
||||
format!("거더 {}-{}", s + 1, i + 1)
|
||||
} else {
|
||||
@@ -534,7 +592,7 @@ pub fn build_selectable_scene<K: GeomKernel>(
|
||||
}
|
||||
}
|
||||
|
||||
// Deck Slab (전 구간 연속)
|
||||
// Deck Slab (전 구간 연속, 경간별 camber)
|
||||
let half_w = ((n_girders as f32 - 1.0) * spacing) * 0.5 + 1_000.0;
|
||||
let deck_ir = DeckSlabIR {
|
||||
id: FeatureId::new(), station_start: 0.0, station_end: total_m,
|
||||
@@ -545,6 +603,10 @@ pub fn build_selectable_scene<K: GeomKernel>(
|
||||
let mut deck = kernel.deck_slab_mesh(&deck_ir)?;
|
||||
deck.recolor(COL_DECK);
|
||||
for v in &mut deck.vertices { v[1] += girder_h + p.slab_thickness; }
|
||||
for s in 0..span_count {
|
||||
let z0 = span_mm * s as f32;
|
||||
apply_camber_mesh(&mut deck, z0, z0 + span_mm, p.camber_mid_mm);
|
||||
}
|
||||
out.push(FeatureMesh { mesh: deck, label: "바닥판 슬래브".into() });
|
||||
|
||||
// Sprint 27: skew rad — 교대·교각·받침·신축이음에 적용.
|
||||
@@ -671,12 +733,11 @@ pub fn build_selectable_scene<K: GeomKernel>(
|
||||
|
||||
// ── Parapets (Sprint 28: 방호벽) ───────────────────────────────────────
|
||||
// 양쪽 데크 엣지를 따라 RC 방호벽 (높이 1200mm, 두께 500mm, 전 구간 연속).
|
||||
// 데크 상면 (Y=girder_h+slab) 위에 서는 단순 박스.
|
||||
{
|
||||
const PARAPET_H: f32 = 1_200.0;
|
||||
const PARAPET_T: f32 = 500.0;
|
||||
let y_base = girder_h + 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, "좌")] {
|
||||
let profile = vec![
|
||||
[x_center - PARAPET_T * 0.5, 0.0],
|
||||
@@ -685,12 +746,38 @@ pub fn build_selectable_scene<K: GeomKernel>(
|
||||
[x_center - PARAPET_T * 0.5, PARAPET_H],
|
||||
];
|
||||
let mut mesh = cimery_kernel::sweep::sweep_profile_flat(&profile, total_mm);
|
||||
mesh.recolor(COL_ABUTMENT); // parapet concrete color
|
||||
mesh.recolor(COL_ABUTMENT);
|
||||
for v in &mut mesh.vertices { v[1] += y_base; }
|
||||
out.push(FeatureMesh { mesh, label: format!("방호벽 ({})", side_label) });
|
||||
}
|
||||
}
|
||||
|
||||
// ── Diaphragms (Sprint 29: 지점부 격벽) ────────────────────────────────
|
||||
if p.show_diaphragms {
|
||||
const DIA_THICK: f32 = 300.0;
|
||||
const WEB_CLEAR: f32 = 250.0;
|
||||
let dia_w = spacing - WEB_CLEAR;
|
||||
for (sup_idx, &z) in support_zs.iter().enumerate() {
|
||||
for i in 0..n_girders.saturating_sub(1) {
|
||||
let x_mid = ((i as f32) - (n_girders as f32 - 1.0) * 0.5 + 0.5) * spacing;
|
||||
let profile = vec![
|
||||
[x_mid - dia_w * 0.5, 0.0],
|
||||
[x_mid + dia_w * 0.5, 0.0],
|
||||
[x_mid + dia_w * 0.5, girder_h],
|
||||
[x_mid - dia_w * 0.5, girder_h],
|
||||
];
|
||||
let mut mesh = cimery_kernel::sweep::sweep_profile_flat(&profile, DIA_THICK);
|
||||
mesh.recolor(COL_DIAPHRAGM);
|
||||
for v in &mut mesh.vertices { v[2] += z - DIA_THICK * 0.5; }
|
||||
mesh = rotate_y_around_z(mesh, skew_rad, z);
|
||||
let side = if sup_idx == 0 { "시작".to_string() }
|
||||
else if sup_idx == span_count { "종점".to_string() }
|
||||
else { format!("P{}", sup_idx) };
|
||||
out.push(FeatureMesh { mesh, label: format!("격벽 {}-{}", side, i + 1) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
|
||||
@@ -635,6 +635,8 @@ impl RenderState {
|
||||
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("단면 형식");
|
||||
let prev_sec = p.section_type;
|
||||
@@ -668,6 +670,11 @@ impl RenderState {
|
||||
let prev_ej = p.show_expansion_joints;
|
||||
ui.checkbox(&mut p.show_expansion_joints, "신축이음 (Exp. Joint)");
|
||||
if prev_ej != p.show_expansion_joints { dirty = true; }
|
||||
|
||||
// Sprint 29: 격벽
|
||||
let prev_d = p.show_diaphragms;
|
||||
ui.checkbox(&mut p.show_diaphragms, "격벽 (Diaphragm)");
|
||||
if prev_d != p.show_diaphragms { dirty = true; }
|
||||
});
|
||||
|
||||
// ── 표시 옵션 ─────────────────────────────────────────
|
||||
|
||||
@@ -38,6 +38,12 @@ pub struct ProjectFile {
|
||||
/// 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,
|
||||
}
|
||||
|
||||
fn default_true() -> bool { true }
|
||||
@@ -69,6 +75,8 @@ impl ProjectFile {
|
||||
_ => "single".into(),
|
||||
},
|
||||
skew_deg: p.skew_deg,
|
||||
show_diaphragms: p.show_diaphragms,
|
||||
camber_mid_mm: p.camber_mid_mm,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +101,8 @@ impl ProjectFile {
|
||||
_ => cimery_core::PierType::SingleColumn,
|
||||
},
|
||||
skew_deg: self.skew_deg,
|
||||
show_diaphragms: self.show_diaphragms,
|
||||
camber_mid_mm: self.camber_mid_mm,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user