Sprint 4 — Full bridge scene (Girder×5 + DeckSlab + Bearing×10 + Abutment×2)
viewer/bridge_scene.rs: BridgeScene compositor - 5× PSC-I Girder (2500mm c/c) - DeckSlab (12000mm, 220mm thick, top of girders) - 10× Elastomeric Bearing (5 per abutment end) - 2× ReverseT Abutment (start & end) - sweep::merge_meshes로 단일 메시 합성 - scene_extents()로 카메라 자동 배치 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
139
cimery/crates/viewer/src/bridge_scene.rs
Normal file
139
cimery/crates/viewer/src/bridge_scene.rs
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
//! Full bridge scene compositor — Sprint 4.
|
||||||
|
//!
|
||||||
|
//! Builds a single merged mesh for a simple 40 m single-span PSC-I girder bridge:
|
||||||
|
//! - 5 × PSC-I Girder (2500 mm c/c)
|
||||||
|
//! - 1 × Deck Slab (12000 mm wide)
|
||||||
|
//! - 10 × Elastomeric Bearing (5 per abutment)
|
||||||
|
//! - 2 × Abutment (start & end)
|
||||||
|
//!
|
||||||
|
//! 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, MaterialGrade, SectionType};
|
||||||
|
use cimery_ir::{
|
||||||
|
AbutmentIR, BearingIR, DeckSlabIR, FeatureId, GirderIR,
|
||||||
|
PscISectionParams, SectionParams, WingWallIR,
|
||||||
|
};
|
||||||
|
use cimery_kernel::{GeomKernel, KernelError, Mesh};
|
||||||
|
|
||||||
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn translate(mut mesh: Mesh, dx: f32, dy: f32, dz: f32) -> Mesh {
|
||||||
|
for v in &mut mesh.vertices {
|
||||||
|
v[0] += dx;
|
||||||
|
v[1] += dy;
|
||||||
|
v[2] += dz;
|
||||||
|
}
|
||||||
|
mesh
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge(meshes: Vec<Mesh>) -> Mesh {
|
||||||
|
cimery_kernel::sweep::merge_meshes(meshes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Scene builder ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Build a complete bridge scene mesh using the provided kernel.
|
||||||
|
pub fn build_bridge_scene<K: GeomKernel>(kernel: &K) -> Result<Mesh, KernelError> {
|
||||||
|
const SPAN_M: f64 = 40.0;
|
||||||
|
const SPAN_MM: f32 = 40_000.0;
|
||||||
|
const N_GIRDERS: usize = 5;
|
||||||
|
const SPACING: f32 = 2_500.0; // mm c/c
|
||||||
|
const GIRDER_H: f32 = 1_800.0; // mm
|
||||||
|
const BEARING_H: f32 = 60.0; // mm
|
||||||
|
|
||||||
|
let mut parts: Vec<Mesh> = 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(PscISectionParams::kds_standard()),
|
||||||
|
count: 1,
|
||||||
|
spacing: 0.0,
|
||||||
|
material: MaterialGrade::C50,
|
||||||
|
};
|
||||||
|
let mesh = kernel.girder_mesh(&ir)?;
|
||||||
|
parts.push(translate(mesh, x, 0.0, 0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 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
|
||||||
|
let deck_ir = DeckSlabIR {
|
||||||
|
id: FeatureId::new(),
|
||||||
|
station_start: 0.0,
|
||||||
|
station_end: SPAN_M,
|
||||||
|
width_left: half_width as f64,
|
||||||
|
width_right: half_width as f64,
|
||||||
|
thickness: 220.0,
|
||||||
|
haunch_depth: 0.0,
|
||||||
|
cross_slope: 2.0,
|
||||||
|
material: MaterialGrade::C40,
|
||||||
|
};
|
||||||
|
let deck_mesh = kernel.deck_slab_mesh(&deck_ir)?;
|
||||||
|
// Slab Y=0 is its top face; place it so bottom aligns with girder top
|
||||||
|
parts.push(translate(deck_mesh, 0.0, GIRDER_H + 220.0, 0.0));
|
||||||
|
|
||||||
|
// ── Bearings ───────────────────────────────────────────────────────────────
|
||||||
|
// 5 per abutment, one under each girder
|
||||||
|
for &z in &[0.0_f32, SPAN_MM] {
|
||||||
|
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 },
|
||||||
|
bearing_type: BearingType::Elastomeric,
|
||||||
|
plan_length: 350.0,
|
||||||
|
plan_width: 450.0,
|
||||||
|
total_height: BEARING_H as f64,
|
||||||
|
capacity_vertical: 1_500.0,
|
||||||
|
};
|
||||||
|
let mesh = kernel.bearing_mesh(&bearing_ir)?;
|
||||||
|
// Place bearing centred under each girder, top at Y=0 (girder soffit)
|
||||||
|
parts.push(translate(mesh, x - 175.0, -BEARING_H, z - 225.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 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; // incl. overhangs
|
||||||
|
|
||||||
|
for &(station, z) in &[(0.0f64, -800.0_f32), (SPAN_M, SPAN_MM)] {
|
||||||
|
let abut_ir = AbutmentIR {
|
||||||
|
id: FeatureId::new(),
|
||||||
|
station,
|
||||||
|
skew_angle: 0.0,
|
||||||
|
abutment_type: AbutmentType::ReverseT,
|
||||||
|
breast_wall_height: (GIRDER_H + BEARING_H) as f64,
|
||||||
|
breast_wall_thickness: 800.0,
|
||||||
|
breast_wall_width: total_w,
|
||||||
|
footing_length: 4_000.0,
|
||||||
|
footing_width: total_w + 1_000.0,
|
||||||
|
footing_thickness: 1_000.0,
|
||||||
|
wing_wall_left: wing.clone(),
|
||||||
|
wing_wall_right: wing.clone(),
|
||||||
|
material: MaterialGrade::C40,
|
||||||
|
};
|
||||||
|
let mesh = kernel.abutment_mesh(&abut_ir)?;
|
||||||
|
// Place abutment: breast wall top at Y = -(BEARING_H)
|
||||||
|
let y = -(BEARING_H + abut_ir.breast_wall_height as f32);
|
||||||
|
parts.push(translate(mesh, -(total_w as f32) * 0.5, y, z));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(merge(parts))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bounding box of the full bridge scene (for camera setup).
|
||||||
|
pub fn scene_extents() -> ([f32; 3], [f32; 3]) {
|
||||||
|
const SPAN_MM: f32 = 40_000.0;
|
||||||
|
const HALF_W: f32 = 6_500.0;
|
||||||
|
const TOP_Y: f32 = 2_020.0; // top of slab
|
||||||
|
const BOT_Y: f32 = -3_000.0; // footing bottom approx
|
||||||
|
([-HALF_W, BOT_Y, -2_000.0], [HALF_W, TOP_Y, SPAN_MM + 2_000.0])
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
//! - Add selection highlight.
|
//! - Add selection highlight.
|
||||||
|
|
||||||
pub mod camera;
|
pub mod camera;
|
||||||
|
pub mod bridge_scene;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use bytemuck::{Pod, Zeroable};
|
use bytemuck::{Pod, Zeroable};
|
||||||
@@ -31,6 +32,7 @@ use cimery_kernel::OcctKernel;
|
|||||||
use cimery_kernel::PureRustKernel;
|
use cimery_kernel::PureRustKernel;
|
||||||
use cimery_kernel::GeomKernel;
|
use cimery_kernel::GeomKernel;
|
||||||
use camera::Camera;
|
use camera::Camera;
|
||||||
|
use glam;
|
||||||
|
|
||||||
// ─── Vertex ───────────────────────────────────────────────────────────────────
|
// ─── Vertex ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -139,23 +141,14 @@ impl RenderState {
|
|||||||
// ── Depth texture ─────────────────────────────────────────────────────
|
// ── Depth texture ─────────────────────────────────────────────────────
|
||||||
let depth_view = Self::make_depth_view(&device, &surface_config);
|
let depth_view = Self::make_depth_view(&device, &surface_config);
|
||||||
|
|
||||||
// ── Test girder mesh via StubKernel ───────────────────────────────────
|
// ── Full bridge scene (Sprint 4) ──────────────────────────────────────
|
||||||
// Sprint 3: replace StubKernel with OcctKernel when OCCT compiles.
|
// Girder + DeckSlab + Bearing + Abutment
|
||||||
let test_ir = GirderIR {
|
|
||||||
id: FeatureId::new(),
|
|
||||||
station_start: 0.0,
|
|
||||||
station_end: 40.0,
|
|
||||||
offset_from_alignment: 0.0,
|
|
||||||
section_type: SectionType::PscI,
|
|
||||||
section: SectionParams::PscI(PscISectionParams::kds_standard()),
|
|
||||||
count: 1,
|
|
||||||
spacing: 0.0,
|
|
||||||
material: MaterialGrade::C50,
|
|
||||||
};
|
|
||||||
#[cfg(feature = "occt")]
|
#[cfg(feature = "occt")]
|
||||||
let mesh = OcctKernel.girder_mesh(&test_ir).expect("OcctKernel mesh");
|
let mesh = bridge_scene::build_bridge_scene(&OcctKernel)
|
||||||
|
.expect("OcctKernel bridge scene");
|
||||||
#[cfg(not(feature = "occt"))]
|
#[cfg(not(feature = "occt"))]
|
||||||
let mesh = PureRustKernel.girder_mesh(&test_ir).expect("PureRustKernel mesh");
|
let mesh = bridge_scene::build_bridge_scene(&PureRustKernel)
|
||||||
|
.expect("PureRustKernel bridge scene");
|
||||||
|
|
||||||
let verts: Vec<Vertex> = mesh.vertices.iter().zip(mesh.normals.iter())
|
let verts: Vec<Vertex> = mesh.vertices.iter().zip(mesh.normals.iter())
|
||||||
.map(|(p, n)| Vertex { position: *p, normal: *n })
|
.map(|(p, n)| Vertex { position: *p, normal: *n })
|
||||||
@@ -174,7 +167,23 @@ impl RenderState {
|
|||||||
let num_indices = mesh.indices.len() as u32;
|
let num_indices = mesh.indices.len() as u32;
|
||||||
|
|
||||||
// ── Camera ────────────────────────────────────────────────────────────
|
// ── Camera ────────────────────────────────────────────────────────────
|
||||||
let mut camera = Camera::default_for_girder(mesh.aabb().1[2]); // span from AABB
|
// Camera for full bridge scene
|
||||||
|
let (mn, mx) = bridge_scene::scene_extents();
|
||||||
|
let cx = (mn[0] + mx[0]) * 0.5;
|
||||||
|
let cy = (mn[1] + mx[1]) * 0.5;
|
||||||
|
let cz = (mn[2] + mx[2]) * 0.5;
|
||||||
|
let span = (mx[2] - mn[2]).max(mx[0] - mn[0]);
|
||||||
|
let mut camera = Camera {
|
||||||
|
target: glam::Vec3::new(cx, cy, cz),
|
||||||
|
radius: span * 1.2,
|
||||||
|
yaw: std::f32::consts::FRAC_PI_4,
|
||||||
|
pitch: 0.30,
|
||||||
|
fov_y: 60.0_f32.to_radians(),
|
||||||
|
aspect: 16.0 / 9.0,
|
||||||
|
znear: 10.0,
|
||||||
|
zfar: 10_000_000.0,
|
||||||
|
};
|
||||||
|
let _ = mesh.aabb(); // keep aabb call for future use
|
||||||
camera.resize(surface_config.width, surface_config.height);
|
camera.resize(surface_config.width, surface_config.height);
|
||||||
|
|
||||||
let camera_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
let camera_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||||
@@ -384,9 +393,9 @@ impl ApplicationHandler for CimeryApp {
|
|||||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
let attrs = Window::default_attributes()
|
let attrs = Window::default_attributes()
|
||||||
.with_title(if cfg!(feature = "occt") {
|
.with_title(if cfg!(feature = "occt") {
|
||||||
"cimery viewer [Sprint 3 — OcctKernel B-rep]"
|
"cimery viewer [Sprint 4 — Full Bridge / OcctKernel]"
|
||||||
} else {
|
} else {
|
||||||
"cimery viewer [Sprint 2 — PSC-I PureRustKernel]"
|
"cimery viewer [Sprint 4 — Full Bridge / PureRustKernel]"
|
||||||
})
|
})
|
||||||
.with_inner_size(winit::dpi::LogicalSize::new(1280u32, 720u32));
|
.with_inner_size(winit::dpi::LogicalSize::new(1280u32, 720u32));
|
||||||
let window = Arc::new(
|
let window = Arc::new(
|
||||||
|
|||||||
Reference in New Issue
Block a user