Sprint 25/26 — 단면 분기 수정 + 다경간 + 피어 배치
Sprint 25: build_selectable_scene 의 SectionType::PscI 하드코딩 제거. - p.section_type 에 따라 PscI / SteelBox 분기 (build_bridge_scene 과 동일 로직). - 사용자가 단면 형식 전환 시 정상 반영. Sprint 26: 다경간 + 교각 배치 (ParaWiki 교각 wiki Phase 1 MVP). - SceneParams: span_count (1~5) + pier_type (SingleColumn/MultiColumn) 추가. - span_m 의미 변경: 전체 교량 길이 → 경간당 길이. total = span_m × span_count. - pier_ir_for_params() helper: 기본 사각 기둥·2m CSB·2.5m TB·5m column_height. - build_bridge_scene / build_selectable_scene: · 거더: 경간마다 독립 세트 (span_count × girder_count 개) · 데크 슬래브: 전 구간 연속 1개 · 받침: 모든 지점 (교대 2 + 교각 span_count-1) · 교각: 내부 지점 span_count-1 개 (새 기능) · 교대: 양 끝 (z=-800, z=total_mm) · 가로보: 경간마다 반복 · 신축이음: 모든 지점 (선택적) - build_background_scene + scene_extents: total_mm 기준으로 ground·alignment 길이 확장. - project_file: version=2, span_count·pier_type 필드 추가 (v1 호환 default 값). - UI ribbon: "경간 수" 슬라이더 + "교각 형식" T/π 선택. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,10 +9,13 @@
|
||||
//! Positions are in the same coordinate space as the girder mesh:
|
||||
//! X = transverse (right = +), Y = vertical (up = +), Z = along span.
|
||||
|
||||
use cimery_core::{AbutmentType, BearingType, CrossBeamSection, ExpansionJointType, MaterialGrade, SectionType};
|
||||
use cimery_core::{
|
||||
AbutmentType, BearingType, ColumnShape, CrossBeamSection, ExpansionJointType,
|
||||
MaterialGrade, PierType, SectionType,
|
||||
};
|
||||
use cimery_ir::{
|
||||
AbutmentIR, BearingIR, CrossBeamIR, DeckSlabIR, ExpansionJointIR, FeatureId, GirderIR,
|
||||
PscISectionParams, SectionParams, WingWallIR,
|
||||
AbutmentIR, BearingIR, CapBeamIR, CrossBeamIR, DeckSlabIR, ExpansionJointIR, FeatureId,
|
||||
GirderIR, PierIR, PscISectionParams, SectionParams, WingWallIR,
|
||||
};
|
||||
use cimery_kernel::{GeomKernel, KernelError, Mesh};
|
||||
|
||||
@@ -26,8 +29,13 @@ pub enum GirderSectionType { PscI, SteelBox }
|
||||
/// Changing any field and calling `build_bridge_scene` regenerates the mesh.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SceneParams {
|
||||
/// Girder span [m]. Range 20–80 m.
|
||||
/// Length of ONE span [m]. Range 20–80 m. Sprint 26 의미 변경: 경간 = 교각·교대
|
||||
/// 간 한 구간의 길이 (전체 교량 길이 = span_m × span_count).
|
||||
pub span_m: f64,
|
||||
/// 경간 수 (Sprint 26). 1 = 단경간(피어 없음), 2+ = 다경간(내부에 피어 배치).
|
||||
pub span_count: usize,
|
||||
/// 교각 형식 (Sprint 26). SingleColumn=T형, MultiColumn=π형.
|
||||
pub pier_type: PierType,
|
||||
/// Number of girders (3–6).
|
||||
pub girder_count: usize,
|
||||
/// Girder centre-to-centre spacing [mm].
|
||||
@@ -52,6 +60,8 @@ impl Default for SceneParams {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
span_m: 40.0,
|
||||
span_count: 1,
|
||||
pier_type: PierType::SingleColumn,
|
||||
girder_count: 5,
|
||||
girder_spacing: 2_500.0,
|
||||
girder_height: 1_800.0,
|
||||
@@ -74,6 +84,48 @@ pub const COL_GROUND: [f32; 3] = [0.35, 0.38, 0.30]; // dark olive ground
|
||||
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)
|
||||
|
||||
// ─── Pier helper (Sprint 26) ─────────────────────────────────────────────────
|
||||
|
||||
/// 교각 IR 생성 — MVP Phase 1 (wiki 파라미터 카탈로그 30% 입력만 반영).
|
||||
/// 단면 직사각형 고정, CSB/CSL 은 기본값. 컬럼 수는 pier_type 으로 결정.
|
||||
pub fn pier_ir_for_params(
|
||||
station_m: f64,
|
||||
n_girders: usize,
|
||||
spacing_mm: f32,
|
||||
girder_h_mm: f32,
|
||||
pier_type: PierType,
|
||||
) -> PierIR {
|
||||
let n = n_girders as f64;
|
||||
let total_trans = (n - 1.0) * spacing_mm as f64 + 2_000.0;
|
||||
let column_count = match pier_type {
|
||||
PierType::SingleColumn => 1,
|
||||
PierType::MultiColumn => 2,
|
||||
_ => 1,
|
||||
};
|
||||
let col_spacing = if column_count > 1 { total_trans / 2.0 } else { 0.0 };
|
||||
PierIR {
|
||||
id: FeatureId::new(),
|
||||
station: station_m,
|
||||
skew_angle: 0.0,
|
||||
pier_type,
|
||||
column_shape: ColumnShape::Rectangular,
|
||||
column_count: column_count as u32,
|
||||
column_spacing: col_spacing,
|
||||
column_diameter: 2_000.0, // wiki CSB 매트릭스 (9-13k × ≤15k 영역)
|
||||
column_depth: 2_000.0,
|
||||
column_height: girder_h_mm as f64 + 5_000.0,
|
||||
cap_beam: CapBeamIR {
|
||||
length: total_trans,
|
||||
width: 2_500.0, // wiki TB ≥ 2500
|
||||
depth: 1_500.0,
|
||||
cantilever_left: 800.0,
|
||||
cantilever_right: 800.0,
|
||||
},
|
||||
material: MaterialGrade::C40,
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -93,9 +145,13 @@ fn merge(meshes: Vec<Mesh>) -> Mesh {
|
||||
// ─── Scene builder ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Build a complete bridge scene mesh using the provided kernel and parameters.
|
||||
/// Sprint 26: 다경간 + 피어 배치 지원 (span_count 로 제어).
|
||||
pub fn build_bridge_scene<K: GeomKernel>(kernel: &K, p: &SceneParams) -> Result<Mesh, KernelError> {
|
||||
let span_m = p.span_m;
|
||||
let span_mm = (p.span_m * 1_000.0) as f32;
|
||||
let span_count = p.span_count.max(1).min(5);
|
||||
let total_m = span_m * span_count as f64;
|
||||
let total_mm = span_mm * span_count as f32;
|
||||
let n_girders = p.girder_count.max(1).min(10);
|
||||
let spacing = p.girder_spacing;
|
||||
let girder_h = p.girder_height;
|
||||
@@ -132,32 +188,35 @@ pub fn build_bridge_scene<K: GeomKernel>(kernel: &K, p: &SceneParams) -> Result<
|
||||
GirderSectionType::SteelBox => SectionType::SteelBox,
|
||||
};
|
||||
|
||||
// ── Girders ────────────────────────────────────────────────────────────────
|
||||
for i in 0..n_girders {
|
||||
let x = (i as f32 - (n_girders as f32 - 1.0) * 0.5) * spacing;
|
||||
let ir = GirderIR {
|
||||
id: FeatureId::new(),
|
||||
station_start: 0.0,
|
||||
station_end: span_m,
|
||||
offset_from_alignment: x as f64,
|
||||
section_type: section_type_enum,
|
||||
section: section_enum.clone(),
|
||||
count: 1,
|
||||
spacing: 0.0,
|
||||
material: MaterialGrade::C50,
|
||||
};
|
||||
let mut mesh = kernel.girder_mesh(&ir)?;
|
||||
mesh.recolor(COL_GIRDER);
|
||||
parts.push(translate(mesh, x, 0.0, 0.0));
|
||||
// ── Girders (경간마다 독립 세트) ───────────────────────────────────────────
|
||||
for s in 0..span_count {
|
||||
let z_base = span_mm * s as f32;
|
||||
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;
|
||||
let ir = GirderIR {
|
||||
id: FeatureId::new(),
|
||||
station_start: s_start,
|
||||
station_end: s_start + span_m,
|
||||
offset_from_alignment: x as f64,
|
||||
section_type: section_type_enum,
|
||||
section: section_enum.clone(),
|
||||
count: 1,
|
||||
spacing: 0.0,
|
||||
material: MaterialGrade::C50,
|
||||
};
|
||||
let mut mesh = kernel.girder_mesh(&ir)?;
|
||||
mesh.recolor(COL_GIRDER);
|
||||
parts.push(translate(mesh, x, 0.0, z_base));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Deck Slab ──────────────────────────────────────────────────────────────
|
||||
// KDS: min 220 mm, width = (N-1)*spacing + 2 × cantilever
|
||||
let half_width = ((n_girders as f32 - 1.0) * spacing) * 0.5 + 1_000.0; // 1 m cantilever
|
||||
// ── Deck Slab (전 구간 연속) ───────────────────────────────────────────────
|
||||
let half_width = ((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: span_m,
|
||||
station_end: total_m,
|
||||
width_left: half_width as f64,
|
||||
width_right: half_width as f64,
|
||||
thickness: p.slab_thickness as f64,
|
||||
@@ -169,17 +228,16 @@ pub fn build_bridge_scene<K: GeomKernel>(kernel: &K, p: &SceneParams) -> Result<
|
||||
deck_mesh.recolor(COL_DECK);
|
||||
parts.push(translate(deck_mesh, 0.0, girder_h + p.slab_thickness, 0.0));
|
||||
|
||||
// ── Bearings ───────────────────────────────────────────────────────────────
|
||||
// 5 per abutment, one under each girder
|
||||
// plan_length(350mm) = 경간 방향 → half = 175mm 으로 Z 센터링.
|
||||
// ── Bearings (모든 지점: 교대 2 + 교각 span_count-1) ──────────────────────
|
||||
const BEARING_PLAN_LEN: f32 = 350.0;
|
||||
const BEARING_PLAN_WID: f32 = 450.0;
|
||||
for &z in &[0.0_f32, span_mm] {
|
||||
let support_zs: Vec<f32> = (0..=span_count).map(|i| span_mm * i as f32).collect();
|
||||
for &z in &support_zs {
|
||||
for i in 0..n_girders {
|
||||
let x = (i as f32 - (n_girders as f32 - 1.0) * 0.5) * spacing;
|
||||
let bearing_ir = BearingIR {
|
||||
id: FeatureId::new(),
|
||||
station: if z < 1.0 { 0.0 } else { span_m },
|
||||
station: z as f64 / 1_000.0,
|
||||
bearing_type: BearingType::Elastomeric,
|
||||
plan_length: BEARING_PLAN_LEN as f64,
|
||||
plan_width: BEARING_PLAN_WID as f64,
|
||||
@@ -188,17 +246,30 @@ pub fn build_bridge_scene<K: GeomKernel>(kernel: &K, p: &SceneParams) -> Result<
|
||||
};
|
||||
let mut mesh = kernel.bearing_mesh(&bearing_ir)?;
|
||||
mesh.recolor(COL_BEARING);
|
||||
// Z 중심 = 교대 위치(z). bearing mesh Z = [0, plan_length] → 오프셋 = z - plan_length/2.
|
||||
parts.push(translate(mesh, x, 0.0, z - BEARING_PLAN_LEN * 0.5));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Abutments ──────────────────────────────────────────────────────────────
|
||||
// ── Piers (내부 지점 span_count-1 개, Sprint 26) ──────────────────────────
|
||||
for s in 1..span_count {
|
||||
let pier_station_m = span_m * s as f64;
|
||||
let pier_z = span_mm * s as f32;
|
||||
let pier_ir = pier_ir_for_params(
|
||||
pier_station_m, n_girders, spacing, girder_h, p.pier_type,
|
||||
);
|
||||
let mut mesh = kernel.pier_mesh(&pier_ir)?;
|
||||
mesh.recolor(COL_PIER);
|
||||
// pier_mesh 로컬 좌표: cap beam 상면이 Y=0 (거더 소핏 아래 bearing seat).
|
||||
// Y 오프셋은 pier_mesh 자체가 정의하는 로컬 → 변환 불필요 (0).
|
||||
parts.push(translate(mesh, 0.0, -BEARING_H, pier_z));
|
||||
}
|
||||
|
||||
// ── Abutments (양 끝) ─────────────────────────────────────────────────────
|
||||
let wing = WingWallIR { length: 5_000.0, height: 2_500.0, thickness: 500.0 };
|
||||
let total_w = (n_girders as f64 - 1.0) * spacing as f64 + 3_000.0;
|
||||
let breast_wall_h = (girder_h + BEARING_H) as f64;
|
||||
|
||||
for &(station, z) in &[(0.0f64, -800.0_f32), (span_m, span_mm)] {
|
||||
for &(station, z) in &[(0.0f64, -800.0_f32), (total_m, total_mm)] {
|
||||
let abut_ir = AbutmentIR {
|
||||
id: FeatureId::new(),
|
||||
station,
|
||||
@@ -222,30 +293,30 @@ pub fn build_bridge_scene<K: GeomKernel>(kernel: &K, p: &SceneParams) -> Result<
|
||||
|
||||
// ── Ground plane ───────────────────────────────────────────────────────────
|
||||
{
|
||||
let ground_y = -(BEARING_H + breast_wall_h as f32 + 1_000.0 + 200.0); // footing bottom - margin
|
||||
let hw = total_w as f32 * 0.5 + 8_000.0; // wider than bridge
|
||||
let half_z = span_mm * 0.5 + 8_000.0;
|
||||
let ground_y = -(BEARING_H + breast_wall_h as f32 + 1_000.0 + 200.0);
|
||||
let hw = total_w as f32 * 0.5 + 8_000.0;
|
||||
let half_z = total_mm * 0.5 + 8_000.0;
|
||||
let thickness = 500.0_f32;
|
||||
let profile = vec![
|
||||
[-hw, -thickness], [hw, -thickness], [hw, 0.0], [-hw, 0.0],
|
||||
];
|
||||
let mut ground = cimery_kernel::sweep::sweep_profile_flat(&profile, half_z * 2.0);
|
||||
ground.recolor(COL_GROUND);
|
||||
parts.push(translate(ground, 0.0, ground_y, -half_z));
|
||||
parts.push(translate(ground, 0.0, ground_y, total_mm * 0.5 - half_z));
|
||||
}
|
||||
|
||||
// ── Alignment centreline (optional) ───────────────────────────────────────
|
||||
if p.show_alignment {
|
||||
let radius = 80.0_f32;
|
||||
let mut align = cimery_kernel::sweep::polygon_prism(0.0, 0.0, radius, 8, span_mm);
|
||||
let mut align = cimery_kernel::sweep::polygon_prism(0.0, 0.0, radius, 8, total_mm);
|
||||
align.recolor(COL_ALIGNMENT);
|
||||
parts.push(translate(align, 0.0, girder_h * 0.5, 0.0));
|
||||
}
|
||||
|
||||
// ── Cross Beams (Sprint 19) ────────────────────────────────────────────────
|
||||
// ── Cross Beams (경간마다) ────────────────────────────────────────────────
|
||||
if p.show_cross_beams {
|
||||
let interval_mm = (p.cross_beam_interval_m * 1_000.0) as f32;
|
||||
let num_beams = (span_mm / interval_mm).floor() as usize;
|
||||
let num_per_span = (span_mm / interval_mm).floor() as usize;
|
||||
let cb_ir_base = CrossBeamIR {
|
||||
id: FeatureId::new(),
|
||||
station: 0.0,
|
||||
@@ -258,26 +329,29 @@ pub fn build_bridge_scene<K: GeomKernel>(kernel: &K, p: &SceneParams) -> Result<
|
||||
girder_spacing: spacing as f64,
|
||||
material: MaterialGrade::C50,
|
||||
};
|
||||
for i in 0..num_beams {
|
||||
let z = interval_mm * (i as f32 + 1.0);
|
||||
let mut ir = cb_ir_base.clone();
|
||||
ir.id = FeatureId::new();
|
||||
ir.station = z as f64 / 1_000.0;
|
||||
if let Ok(mut mesh) = kernel.cross_beam_mesh(&ir) {
|
||||
mesh.recolor(COL_CROSS_BEAM);
|
||||
parts.push(translate(mesh, 0.0, 0.0, z));
|
||||
for s in 0..span_count {
|
||||
let z_base = span_mm * s as f32;
|
||||
for i in 0..num_per_span {
|
||||
let z = z_base + interval_mm * (i as f32 + 1.0);
|
||||
let mut ir = cb_ir_base.clone();
|
||||
ir.id = FeatureId::new();
|
||||
ir.station = z as f64 / 1_000.0;
|
||||
if let Ok(mut mesh) = kernel.cross_beam_mesh(&ir) {
|
||||
mesh.recolor(COL_CROSS_BEAM);
|
||||
parts.push(translate(mesh, 0.0, 0.0, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Expansion Joints (Sprint 19) ──────────────────────────────────────────
|
||||
// ── Expansion Joints (양 교대 + 내부 피어 위치) ──────────────────────────
|
||||
if p.show_expansion_joints {
|
||||
let deck_w = ((n_girders as f32 - 1.0) * spacing + 2_000.0) as f64;
|
||||
let y_top = girder_h + p.slab_thickness;
|
||||
for &(station, z) in &[(0.0f64, 0.0_f32), (span_m, span_mm)] {
|
||||
for &z in &support_zs {
|
||||
let ej_ir = ExpansionJointIR {
|
||||
id: FeatureId::new(),
|
||||
station,
|
||||
station: z as f64 / 1_000.0,
|
||||
joint_type: ExpansionJointType::RubberType,
|
||||
gap_width: 50.0,
|
||||
total_width: deck_w,
|
||||
@@ -297,11 +371,14 @@ pub fn build_bridge_scene<K: GeomKernel>(kernel: &K, p: &SceneParams) -> Result<
|
||||
// ─── Background scene (ground + alignment only, no features) ─────────────────
|
||||
|
||||
/// Returns only the non-selectable background elements: ground plane + alignment line.
|
||||
/// Sprint 26: 다경간 지원 — ground·alignment 가 전체 교량 길이 기준.
|
||||
pub fn build_background_scene(p: &SceneParams) -> Mesh {
|
||||
let span_mm = (p.span_m * 1_000.0) as f32;
|
||||
let girder_h = p.girder_height;
|
||||
let n_girders = p.girder_count.max(1).min(10);
|
||||
let spacing = p.girder_spacing;
|
||||
let span_mm = (p.span_m * 1_000.0) as f32;
|
||||
let span_count = p.span_count.max(1).min(5);
|
||||
let total_mm = span_mm * span_count as f32;
|
||||
let girder_h = p.girder_height;
|
||||
let n_girders = p.girder_count.max(1).min(10);
|
||||
let spacing = p.girder_spacing;
|
||||
const BEARING_H: f32 = 60.0;
|
||||
let breast_wall_h = (girder_h + BEARING_H) as f64;
|
||||
let total_w = (n_girders as f64 - 1.0) * spacing as f64 + 3_000.0;
|
||||
@@ -309,17 +386,17 @@ pub fn build_background_scene(p: &SceneParams) -> Mesh {
|
||||
|
||||
let mut parts: Vec<Mesh> = Vec::new();
|
||||
|
||||
// Ground
|
||||
// Ground — 전체 교량 길이 + 양끝 8m 여유
|
||||
let hw = total_w as f32 * 0.5 + 8_000.0;
|
||||
let half_z = span_mm * 0.5 + 8_000.0;
|
||||
let half_z = total_mm * 0.5 + 8_000.0;
|
||||
let profile = vec![[-hw, -500.0_f32], [hw, -500.0], [hw, 0.0], [-hw, 0.0]];
|
||||
let mut g = cimery_kernel::sweep::sweep_profile_flat(&profile, half_z * 2.0);
|
||||
g.recolor(COL_GROUND);
|
||||
parts.push(translate(g, 0.0, ground_y, -half_z));
|
||||
parts.push(translate(g, 0.0, ground_y, total_mm * 0.5 - half_z));
|
||||
|
||||
// Alignment
|
||||
// Alignment — 전체 교량 길이
|
||||
if p.show_alignment {
|
||||
let mut align = cimery_kernel::sweep::polygon_prism(0.0, 0.0, 80.0, 8, span_mm);
|
||||
let mut align = cimery_kernel::sweep::polygon_prism(0.0, 0.0, 80.0, 8, total_mm);
|
||||
align.recolor(COL_ALIGNMENT);
|
||||
parts.push(translate(align, 0.0, girder_h * 0.5, 0.0));
|
||||
}
|
||||
@@ -346,37 +423,71 @@ pub fn build_selectable_scene<K: GeomKernel>(
|
||||
let girder_h = p.girder_height;
|
||||
const BEARING_H: f32 = 60.0;
|
||||
|
||||
let section = PscISectionParams {
|
||||
total_height: girder_h as f64,
|
||||
top_flange_width: 600.0,
|
||||
top_flange_thickness: 150.0,
|
||||
bottom_flange_width: 700.0,
|
||||
bottom_flange_thickness: 180.0,
|
||||
web_thickness: 200.0,
|
||||
haunch: 50.0,
|
||||
// Sprint 25: 단면 타입 하드코딩 제거. p.section_type 에 따라 PscI / SteelBox 분기.
|
||||
let (section_enum, section_type_enum) = match p.section_type {
|
||||
GirderSectionType::PscI => (
|
||||
SectionParams::PscI(PscISectionParams {
|
||||
total_height: girder_h as f64,
|
||||
top_flange_width: 600.0,
|
||||
top_flange_thickness: 150.0,
|
||||
bottom_flange_width: 700.0,
|
||||
bottom_flange_thickness: 180.0,
|
||||
web_thickness: 200.0,
|
||||
haunch: 50.0,
|
||||
}),
|
||||
SectionType::PscI,
|
||||
),
|
||||
GirderSectionType::SteelBox => {
|
||||
use cimery_ir::SteelBoxParams;
|
||||
let h = girder_h as f64;
|
||||
(
|
||||
SectionParams::SteelBox(SteelBoxParams {
|
||||
total_height: h,
|
||||
top_width: h * 1.2,
|
||||
bottom_width: h * 0.8,
|
||||
web_thickness: 20.0,
|
||||
top_flange_thickness: 25.0,
|
||||
bottom_flange_thickness: 22.0,
|
||||
}),
|
||||
SectionType::SteelBox,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let span_count = p.span_count.max(1).min(5);
|
||||
let total_m = span_m * span_count as f64;
|
||||
let total_mm = span_mm * span_count as f32;
|
||||
|
||||
let mut out: Vec<FeatureMesh> = Vec::new();
|
||||
|
||||
// Girders
|
||||
for i in 0..n_girders {
|
||||
let x = (i as f32 - (n_girders as f32 - 1.0) * 0.5) * spacing;
|
||||
let ir = GirderIR {
|
||||
id: FeatureId::new(), station_start: 0.0, station_end: span_m,
|
||||
offset_from_alignment: x as f64, section_type: SectionType::PscI,
|
||||
section: SectionParams::PscI(section.clone()),
|
||||
count: 1, spacing: 0.0, material: MaterialGrade::C50,
|
||||
};
|
||||
let mut mesh = kernel.girder_mesh(&ir)?;
|
||||
mesh.recolor(COL_GIRDER);
|
||||
for v in &mut mesh.vertices { v[0] += x; }
|
||||
out.push(FeatureMesh { mesh, label: format!("거더 {}", i + 1) });
|
||||
// Girders (경간마다 독립 세트)
|
||||
for s in 0..span_count {
|
||||
let z_base = span_mm * s as f32;
|
||||
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;
|
||||
let ir = GirderIR {
|
||||
id: FeatureId::new(), station_start: s_start, station_end: s_start + span_m,
|
||||
offset_from_alignment: x as f64, section_type: section_type_enum,
|
||||
section: section_enum.clone(),
|
||||
count: 1, spacing: 0.0, material: MaterialGrade::C50,
|
||||
};
|
||||
let mut mesh = kernel.girder_mesh(&ir)?;
|
||||
mesh.recolor(COL_GIRDER);
|
||||
for v in &mut mesh.vertices { v[0] += x; v[2] += z_base; }
|
||||
let label = if span_count > 1 {
|
||||
format!("거더 {}-{}", s + 1, i + 1)
|
||||
} else {
|
||||
format!("거더 {}", i + 1)
|
||||
};
|
||||
out.push(FeatureMesh { mesh, label });
|
||||
}
|
||||
}
|
||||
|
||||
// Deck Slab (one unit)
|
||||
// Deck Slab (전 구간 연속)
|
||||
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: span_m,
|
||||
id: FeatureId::new(), station_start: 0.0, station_end: total_m,
|
||||
width_left: half_w as f64, width_right: half_w as f64,
|
||||
thickness: p.slab_thickness as f64, haunch_depth: 0.0,
|
||||
cross_slope: 2.0, material: MaterialGrade::C40,
|
||||
@@ -386,14 +497,14 @@ pub fn build_selectable_scene<K: GeomKernel>(
|
||||
for v in &mut deck.vertices { v[1] += girder_h + p.slab_thickness; }
|
||||
out.push(FeatureMesh { mesh: deck, label: "바닥판 슬래브".into() });
|
||||
|
||||
// Bearings
|
||||
// plan_length(350mm) = 경간 방향 → Z 센터링 오프셋 = z - 175.
|
||||
// Bearings (모든 지점)
|
||||
const SEL_BEARING_LEN: f32 = 350.0;
|
||||
for &z in &[0.0_f32, span_mm] {
|
||||
let support_zs: Vec<f32> = (0..=span_count).map(|i| span_mm * i as f32).collect();
|
||||
for (sup_idx, &z) in support_zs.iter().enumerate() {
|
||||
for i in 0..n_girders {
|
||||
let x = (i as f32 - (n_girders as f32 - 1.0) * 0.5) * spacing;
|
||||
let bir = BearingIR {
|
||||
id: FeatureId::new(), station: if z < 1.0 { 0.0 } else { span_m },
|
||||
id: FeatureId::new(), station: z as f64 / 1_000.0,
|
||||
bearing_type: BearingType::Elastomeric,
|
||||
plan_length: SEL_BEARING_LEN as f64, plan_width: 450.0,
|
||||
total_height: BEARING_H as f64, capacity_vertical: 1_500.0,
|
||||
@@ -401,16 +512,31 @@ pub fn build_selectable_scene<K: GeomKernel>(
|
||||
let mut mesh = kernel.bearing_mesh(&bir)?;
|
||||
mesh.recolor(COL_BEARING);
|
||||
for v in &mut mesh.vertices { v[0] += x; v[2] += z - SEL_BEARING_LEN * 0.5; }
|
||||
let side = if z < 1.0 { "시작" } else { "종점" };
|
||||
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) });
|
||||
}
|
||||
}
|
||||
|
||||
// Abutments
|
||||
// Piers (내부 지점, Sprint 26)
|
||||
for s in 1..span_count {
|
||||
let pier_station_m = span_m * s as f64;
|
||||
let pier_z = span_mm * s as f32;
|
||||
let pier_ir = pier_ir_for_params(
|
||||
pier_station_m, n_girders, spacing, girder_h, p.pier_type,
|
||||
);
|
||||
let mut mesh = kernel.pier_mesh(&pier_ir)?;
|
||||
mesh.recolor(COL_PIER);
|
||||
for v in &mut mesh.vertices { v[1] -= BEARING_H; v[2] += pier_z; }
|
||||
out.push(FeatureMesh { mesh, label: format!("교각 P{}", s) });
|
||||
}
|
||||
|
||||
// Abutments (양 끝)
|
||||
let total_w = (n_girders as f64 - 1.0) * spacing as f64 + 3_000.0;
|
||||
let bwh = (girder_h + BEARING_H) as f64;
|
||||
let wing = WingWallIR { length: 5_000.0, height: 2_500.0, thickness: 500.0 };
|
||||
for &(station, z) in &[(0.0f64, -800.0_f32), (span_m, span_mm)] {
|
||||
for &(station, z) in &[(0.0f64, -800.0_f32), (total_m, total_mm)] {
|
||||
let air = AbutmentIR {
|
||||
id: FeatureId::new(), station, skew_angle: 0.0,
|
||||
abutment_type: AbutmentType::ReverseT,
|
||||
@@ -428,10 +554,10 @@ pub fn build_selectable_scene<K: GeomKernel>(
|
||||
out.push(FeatureMesh { mesh, label: format!("교대 ({})", side) });
|
||||
}
|
||||
|
||||
// ── Cross Beams (Sprint 19) ────────────────────────────────────────────
|
||||
// Cross Beams (경간마다)
|
||||
if p.show_cross_beams {
|
||||
let interval_mm = (p.cross_beam_interval_m * 1_000.0) as f32;
|
||||
let num_beams = (span_mm / interval_mm).floor() as usize;
|
||||
let interval_mm = (p.cross_beam_interval_m * 1_000.0) as f32;
|
||||
let num_per_span = (span_mm / interval_mm).floor() as usize;
|
||||
let cb_ir_base = CrossBeamIR {
|
||||
id: FeatureId::new(),
|
||||
station: 0.0,
|
||||
@@ -444,29 +570,28 @@ pub fn build_selectable_scene<K: GeomKernel>(
|
||||
girder_spacing: spacing as f64,
|
||||
material: MaterialGrade::C50,
|
||||
};
|
||||
for i in 0..num_beams {
|
||||
let z = interval_mm * (i as f32 + 1.0);
|
||||
let mut ir = cb_ir_base.clone();
|
||||
ir.id = FeatureId::new();
|
||||
ir.station = z as f64 / 1_000.0;
|
||||
let mut mesh = kernel.cross_beam_mesh(&ir)?;
|
||||
mesh.recolor(COL_CROSS_BEAM);
|
||||
let half = ir.total_length_mm() as f32 * 0.5;
|
||||
for v in &mut mesh.vertices {
|
||||
v[2] += z;
|
||||
for s in 0..span_count {
|
||||
let z_base = span_mm * s as f32;
|
||||
for i in 0..num_per_span {
|
||||
let z = z_base + interval_mm * (i as f32 + 1.0);
|
||||
let mut ir = cb_ir_base.clone();
|
||||
ir.id = FeatureId::new();
|
||||
ir.station = z as f64 / 1_000.0;
|
||||
let mut mesh = kernel.cross_beam_mesh(&ir)?;
|
||||
mesh.recolor(COL_CROSS_BEAM);
|
||||
for v in &mut mesh.vertices { v[2] += z; }
|
||||
out.push(FeatureMesh { mesh, label: format!("가로보 @ {:.0}m", z / 1_000.0) });
|
||||
}
|
||||
let _ = half; // translation already applied in cross_beam builder
|
||||
out.push(FeatureMesh { mesh, label: format!("가로보 @ {:.0}m", z / 1_000.0) });
|
||||
}
|
||||
}
|
||||
|
||||
// ── Expansion Joints (Sprint 19) ───────────────────────────────────────
|
||||
// ── Expansion Joints (모든 지점) ───────────────────────────────────────
|
||||
if p.show_expansion_joints {
|
||||
let deck_w = ((n_girders as f32 - 1.0) * spacing + 2_000.0) as f64;
|
||||
for &(station, z) in &[(0.0f64, 0.0_f32), (span_m, span_mm)] {
|
||||
for (sup_idx, &z) in support_zs.iter().enumerate() {
|
||||
let ej_ir = ExpansionJointIR {
|
||||
id: FeatureId::new(),
|
||||
station,
|
||||
station: z as f64 / 1_000.0,
|
||||
joint_type: ExpansionJointType::RubberType,
|
||||
gap_width: 50.0,
|
||||
total_width: deck_w,
|
||||
@@ -480,7 +605,9 @@ pub fn build_selectable_scene<K: GeomKernel>(
|
||||
v[1] += y_top;
|
||||
v[2] += z;
|
||||
}
|
||||
let side = if z < 1.0 { "시작" } else { "종점" };
|
||||
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) });
|
||||
}
|
||||
}
|
||||
@@ -489,10 +616,11 @@ pub fn build_selectable_scene<K: GeomKernel>(
|
||||
}
|
||||
|
||||
/// Bounding box of the full bridge scene (for camera setup).
|
||||
/// Sprint 26: 다경간 지원 → Z 범위가 span_m × span_count.
|
||||
pub fn scene_extents(p: &SceneParams) -> ([f32; 3], [f32; 3]) {
|
||||
let span_mm = (p.span_m * 1_000.0) as f32;
|
||||
let total_mm = (p.span_m * 1_000.0) as f32 * p.span_count.max(1) as f32;
|
||||
let half_w = ((p.girder_count as f32 - 1.0) * p.girder_spacing * 0.5 + 2_000.0).max(5_000.0);
|
||||
let top_y = p.girder_height + p.slab_thickness + 200.0;
|
||||
let bot_y = -(p.girder_height + 3_000.0 + 1_000.0);
|
||||
([-half_w, bot_y, -2_000.0], [half_w, top_y, span_mm + 2_000.0])
|
||||
([-half_w, bot_y, -2_000.0], [half_w, top_y, total_mm + 2_000.0])
|
||||
}
|
||||
|
||||
@@ -624,6 +624,15 @@ impl RenderState {
|
||||
ps!("c/c 간격 (mm)", &mut p.girder_spacing, 1_500.0..=4_000.0, 100.0);
|
||||
ps!("거더 높이 (mm)", &mut p.girder_height, 1_000.0..=3_000.0, 100.0);
|
||||
ps!("슬래브 두께 (mm)",&mut p.slab_thickness, 150.0..=400.0, 10.0);
|
||||
// Sprint 26: 다경간 지원
|
||||
ps!("경간 수", &mut p.span_count, 1..=5, 1.0);
|
||||
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; }
|
||||
|
||||
ui.label("단면 형식");
|
||||
let prev_sec = p.section_type;
|
||||
|
||||
@@ -30,15 +30,22 @@ pub struct ProjectFile {
|
||||
pub cross_beam_interval_m: f64,
|
||||
#[serde(default = "default_true")]
|
||||
pub show_expansion_joints: bool,
|
||||
/// Sprint 26: 다경간
|
||||
#[serde(default = "default_span_count")]
|
||||
pub span_count: usize,
|
||||
#[serde(default = "default_pier_type")]
|
||||
pub pier_type: String, // "single" | "multi"
|
||||
}
|
||||
|
||||
fn default_true() -> bool { true }
|
||||
fn default_cross_beam_interval() -> f64 { 5.0 }
|
||||
fn default_span_count() -> usize { 1 }
|
||||
fn default_pier_type() -> String { "single".into() }
|
||||
|
||||
impl ProjectFile {
|
||||
pub fn from_params(name: &str, p: &SceneParams) -> Self {
|
||||
Self {
|
||||
version: 1,
|
||||
version: 2,
|
||||
name: name.to_owned(),
|
||||
span_m: p.span_m,
|
||||
girder_count: p.girder_count,
|
||||
@@ -53,6 +60,11 @@ impl ProjectFile {
|
||||
show_cross_beams: p.show_cross_beams,
|
||||
cross_beam_interval_m: p.cross_beam_interval_m,
|
||||
show_expansion_joints: p.show_expansion_joints,
|
||||
span_count: p.span_count,
|
||||
pier_type: match p.pier_type {
|
||||
cimery_core::PierType::MultiColumn => "multi".into(),
|
||||
_ => "single".into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +83,11 @@ impl ProjectFile {
|
||||
show_cross_beams: self.show_cross_beams,
|
||||
cross_beam_interval_m: self.cross_beam_interval_m,
|
||||
show_expansion_joints: self.show_expansion_joints,
|
||||
span_count: self.span_count,
|
||||
pier_type: match self.pier_type.as_str() {
|
||||
"multi" => cimery_core::PierType::MultiColumn,
|
||||
_ => cimery_core::PierType::SingleColumn,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user