From 3359475879d051c0fdca6a392e4130b69cf23870 Mon Sep 17 00:00:00 2001 From: minsung Date: Tue, 14 Apr 2026 20:08:21 +0900 Subject: [PATCH] =?UTF-8?q?Sprint=204=20=E2=80=94=20Full=20bridge=20scene?= =?UTF-8?q?=20(Girder=C3=975=20+=20DeckSlab=20+=20Bearing=C3=9710=20+=20Ab?= =?UTF-8?q?utment=C3=972)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- cimery/crates/viewer/src/bridge_scene.rs | 139 +++++++++++++++++++++++ cimery/crates/viewer/src/lib.rs | 45 +++++--- 2 files changed, 166 insertions(+), 18 deletions(-) create mode 100644 cimery/crates/viewer/src/bridge_scene.rs diff --git a/cimery/crates/viewer/src/bridge_scene.rs b/cimery/crates/viewer/src/bridge_scene.rs new file mode 100644 index 0000000..b62eb7e --- /dev/null +++ b/cimery/crates/viewer/src/bridge_scene.rs @@ -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 { + cimery_kernel::sweep::merge_meshes(meshes) +} + +// ─── Scene builder ──────────────────────────────────────────────────────────── + +/// Build a complete bridge scene mesh using the provided kernel. +pub fn build_bridge_scene(kernel: &K) -> Result { + 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 = 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]) +} diff --git a/cimery/crates/viewer/src/lib.rs b/cimery/crates/viewer/src/lib.rs index f157057..2cee892 100644 --- a/cimery/crates/viewer/src/lib.rs +++ b/cimery/crates/viewer/src/lib.rs @@ -12,6 +12,7 @@ //! - Add selection highlight. pub mod camera; +pub mod bridge_scene; use std::sync::Arc; use bytemuck::{Pod, Zeroable}; @@ -31,6 +32,7 @@ use cimery_kernel::OcctKernel; use cimery_kernel::PureRustKernel; use cimery_kernel::GeomKernel; use camera::Camera; +use glam; // ─── Vertex ─────────────────────────────────────────────────────────────────── @@ -139,23 +141,14 @@ impl RenderState { // ── Depth texture ───────────────────────────────────────────────────── let depth_view = Self::make_depth_view(&device, &surface_config); - // ── Test girder mesh via StubKernel ─────────────────────────────────── - // Sprint 3: replace StubKernel with OcctKernel when OCCT compiles. - 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, - }; + // ── Full bridge scene (Sprint 4) ────────────────────────────────────── + // Girder + DeckSlab + Bearing + Abutment #[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"))] - 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 = mesh.vertices.iter().zip(mesh.normals.iter()) .map(|(p, n)| Vertex { position: *p, normal: *n }) @@ -174,7 +167,23 @@ impl RenderState { let num_indices = mesh.indices.len() as u32; // ── 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); 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) { let attrs = Window::default_attributes() .with_title(if cfg!(feature = "occt") { - "cimery viewer [Sprint 3 — OcctKernel B-rep]" + "cimery viewer [Sprint 4 — Full Bridge / OcctKernel]" } else { - "cimery viewer [Sprint 2 — PSC-I PureRustKernel]" + "cimery viewer [Sprint 4 — Full Bridge / PureRustKernel]" }) .with_inner_size(winit::dpi::LogicalSize::new(1280u32, 720u32)); let window = Arc::new(