Sprint 5 — 인터랙티브 파라메트릭 + egui 속성 패널
- SceneParams: 경간/거더수/간격/높이/슬래브두께 — 실시간 변경 가능 - egui SidePanel(좌측): 슬라이더 편집 → ▶ 적용 → 씬 재생성 - rebuild_mesh(): 파라미터 변경 시 GPU 버텍스·인덱스 버퍼 재생성 - wgpu 22 + egui-wgpu 0.29: forget_lifetime() 로 render pass 전달 - 3D 인코더와 egui 인코더 분리 (wgpu 22 lifetime 호환) - build_bridge_scene(&SceneParams): 경간·거더수·간격 파라메트릭 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -23,3 +23,7 @@ pollster = "0.3"
|
|||||||
glam = "0.29"
|
glam = "0.29"
|
||||||
cimery-ir = { workspace = true }
|
cimery-ir = { workspace = true }
|
||||||
cimery-core = { workspace = true }
|
cimery-core = { workspace = true }
|
||||||
|
cimery-incremental = { workspace = true }
|
||||||
|
egui = "0.29"
|
||||||
|
egui-wgpu = "0.29"
|
||||||
|
egui-winit = "0.29"
|
||||||
|
|||||||
@@ -16,6 +16,36 @@ use cimery_ir::{
|
|||||||
};
|
};
|
||||||
use cimery_kernel::{GeomKernel, KernelError, Mesh};
|
use cimery_kernel::{GeomKernel, KernelError, Mesh};
|
||||||
|
|
||||||
|
// ─── Scene parameters (user-editable) ────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Parameters that define the bridge geometry.
|
||||||
|
/// Changing any field and calling `build_bridge_scene` regenerates the mesh.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SceneParams {
|
||||||
|
/// Girder span [m]. Range 20–80 m.
|
||||||
|
pub span_m: f64,
|
||||||
|
/// Number of girders (3–6).
|
||||||
|
pub girder_count: usize,
|
||||||
|
/// Girder centre-to-centre spacing [mm].
|
||||||
|
pub girder_spacing: f32,
|
||||||
|
/// PSC-I total height [mm].
|
||||||
|
pub girder_height: f32,
|
||||||
|
/// Slab thickness [mm].
|
||||||
|
pub slab_thickness: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SceneParams {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
span_m: 40.0,
|
||||||
|
girder_count: 5,
|
||||||
|
girder_spacing: 2_500.0,
|
||||||
|
girder_height: 1_800.0,
|
||||||
|
slab_thickness: 220.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Part colours (linear sRGB) ──────────────────────────────────────────────
|
// ── Part colours (linear sRGB) ──────────────────────────────────────────────
|
||||||
pub const COL_GIRDER: [f32; 3] = [0.85, 0.82, 0.72]; // light concrete
|
pub const COL_GIRDER: [f32; 3] = [0.85, 0.82, 0.72]; // light concrete
|
||||||
pub const COL_DECK: [f32; 3] = [0.72, 0.70, 0.62]; // slightly darker slab
|
pub const COL_DECK: [f32; 3] = [0.72, 0.70, 0.62]; // slightly darker slab
|
||||||
@@ -39,14 +69,14 @@ fn merge(meshes: Vec<Mesh>) -> Mesh {
|
|||||||
|
|
||||||
// ─── Scene builder ────────────────────────────────────────────────────────────
|
// ─── Scene builder ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Build a complete bridge scene mesh using the provided kernel.
|
/// Build a complete bridge scene mesh using the provided kernel and parameters.
|
||||||
pub fn build_bridge_scene<K: GeomKernel>(kernel: &K) -> Result<Mesh, KernelError> {
|
pub fn build_bridge_scene<K: GeomKernel>(kernel: &K, p: &SceneParams) -> Result<Mesh, KernelError> {
|
||||||
const SPAN_M: f64 = 40.0;
|
let SPAN_M = p.span_m;
|
||||||
const SPAN_MM: f32 = 40_000.0;
|
let SPAN_MM = (p.span_m * 1_000.0) as f32;
|
||||||
const N_GIRDERS: usize = 5;
|
let N_GIRDERS = p.girder_count.max(1).min(10);
|
||||||
const SPACING: f32 = 2_500.0; // mm c/c
|
let SPACING = p.girder_spacing;
|
||||||
const GIRDER_H: f32 = 1_800.0; // mm
|
let GIRDER_H = p.girder_height;
|
||||||
const BEARING_H: f32 = 60.0; // mm
|
const BEARING_H: f32 = 60.0; // mm
|
||||||
|
|
||||||
let mut parts: Vec<Mesh> = Vec::new();
|
let mut parts: Vec<Mesh> = Vec::new();
|
||||||
|
|
||||||
@@ -78,14 +108,14 @@ pub fn build_bridge_scene<K: GeomKernel>(kernel: &K) -> Result<Mesh, KernelError
|
|||||||
station_end: SPAN_M,
|
station_end: SPAN_M,
|
||||||
width_left: half_width as f64,
|
width_left: half_width as f64,
|
||||||
width_right: half_width as f64,
|
width_right: half_width as f64,
|
||||||
thickness: 220.0,
|
thickness: p.slab_thickness as f64,
|
||||||
haunch_depth: 0.0,
|
haunch_depth: 0.0,
|
||||||
cross_slope: 2.0,
|
cross_slope: 2.0,
|
||||||
material: MaterialGrade::C40,
|
material: MaterialGrade::C40,
|
||||||
};
|
};
|
||||||
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);
|
||||||
parts.push(translate(deck_mesh, 0.0, GIRDER_H + 220.0, 0.0));
|
parts.push(translate(deck_mesh, 0.0, GIRDER_H + p.slab_thickness, 0.0));
|
||||||
|
|
||||||
// ── Bearings ───────────────────────────────────────────────────────────────
|
// ── Bearings ───────────────────────────────────────────────────────────────
|
||||||
// 5 per abutment, one under each girder
|
// 5 per abutment, one under each girder
|
||||||
@@ -109,7 +139,8 @@ pub fn build_bridge_scene<K: GeomKernel>(kernel: &K) -> Result<Mesh, KernelError
|
|||||||
|
|
||||||
// ── Abutments ──────────────────────────────────────────────────────────────
|
// ── Abutments ──────────────────────────────────────────────────────────────
|
||||||
let wing = WingWallIR { length: 5_000.0, height: 2_500.0, thickness: 500.0 };
|
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
|
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), (SPAN_M, SPAN_MM)] {
|
||||||
let abut_ir = AbutmentIR {
|
let abut_ir = AbutmentIR {
|
||||||
@@ -117,7 +148,7 @@ pub fn build_bridge_scene<K: GeomKernel>(kernel: &K) -> Result<Mesh, KernelError
|
|||||||
station,
|
station,
|
||||||
skew_angle: 0.0,
|
skew_angle: 0.0,
|
||||||
abutment_type: AbutmentType::ReverseT,
|
abutment_type: AbutmentType::ReverseT,
|
||||||
breast_wall_height: (GIRDER_H + BEARING_H) as f64,
|
breast_wall_height: breast_wall_h,
|
||||||
breast_wall_thickness: 800.0,
|
breast_wall_thickness: 800.0,
|
||||||
breast_wall_width: total_w,
|
breast_wall_width: total_w,
|
||||||
footing_length: 4_000.0,
|
footing_length: 4_000.0,
|
||||||
@@ -137,10 +168,10 @@ pub fn build_bridge_scene<K: GeomKernel>(kernel: &K) -> Result<Mesh, KernelError
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Bounding box of the full bridge scene (for camera setup).
|
/// Bounding box of the full bridge scene (for camera setup).
|
||||||
pub fn scene_extents() -> ([f32; 3], [f32; 3]) {
|
pub fn scene_extents(p: &SceneParams) -> ([f32; 3], [f32; 3]) {
|
||||||
const SPAN_MM: f32 = 40_000.0;
|
let span_mm = (p.span_m * 1_000.0) as f32;
|
||||||
const HALF_W: f32 = 6_500.0;
|
let half_w = ((p.girder_count as f32 - 1.0) * p.girder_spacing * 0.5 + 2_000.0).max(5_000.0);
|
||||||
const TOP_Y: f32 = 2_020.0; // top of slab
|
let top_y = p.girder_height + p.slab_thickness + 200.0;
|
||||||
const BOT_Y: f32 = -3_000.0; // footing bottom approx
|
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, span_mm + 2_000.0])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
//! cimery-viewer — Sprint 2.
|
//! cimery-viewer — Sprint 5: Interactive Parametric.
|
||||||
//!
|
//!
|
||||||
//! Renders a PSC-I girder mesh (from StubKernel or OcctKernel) with:
|
//! egui Properties panel (left) + real-time bridge scene regeneration.
|
||||||
//! - Perspective camera (Revit-style orbit: middle-mouse drag + scroll)
|
//! Parameter change → rebuild_mesh() → new GPU buffers → immediate redraw.
|
||||||
//! - Depth buffer
|
|
||||||
//! - Simple directional lighting from surface normals
|
|
||||||
//! - Back-face culling
|
|
||||||
//!
|
|
||||||
//! # Sprint 3 upgrade path
|
|
||||||
//! - Swap `StubKernel` → `OcctKernel` once OCCT compiles.
|
|
||||||
//! - Add ViewCube widget overlay.
|
|
||||||
//! - Add selection highlight.
|
|
||||||
|
|
||||||
pub mod camera;
|
pub mod camera;
|
||||||
pub mod bridge_scene;
|
pub mod bridge_scene;
|
||||||
@@ -30,6 +22,7 @@ use cimery_kernel::OcctKernel;
|
|||||||
use cimery_kernel::PureRustKernel;
|
use cimery_kernel::PureRustKernel;
|
||||||
use camera::{Camera, StandardView};
|
use camera::{Camera, StandardView};
|
||||||
use glam;
|
use glam;
|
||||||
|
use bridge_scene::{SceneParams, build_bridge_scene, scene_extents};
|
||||||
|
|
||||||
// ─── Vertex ───────────────────────────────────────────────────────────────────
|
// ─── Vertex ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -80,12 +73,19 @@ struct RenderState {
|
|||||||
// Depth
|
// Depth
|
||||||
depth_view: wgpu::TextureView,
|
depth_view: wgpu::TextureView,
|
||||||
// Mouse / keyboard state
|
// Mouse / keyboard state
|
||||||
mid_pressed: bool,
|
mid_pressed: bool,
|
||||||
shift_pressed: bool,
|
shift_pressed: bool,
|
||||||
last_mouse: winit::dpi::PhysicalPosition<f64>,
|
last_mouse: winit::dpi::PhysicalPosition<f64>,
|
||||||
// Scene extents for ZoomExtents
|
// Scene extents for ZoomExtents
|
||||||
scene_mn: [f32; 3],
|
scene_mn: [f32; 3],
|
||||||
scene_mx: [f32; 3],
|
scene_mx: [f32; 3],
|
||||||
|
// Scene parameters (user-editable via egui panel)
|
||||||
|
params: SceneParams,
|
||||||
|
dirty: bool, // needs mesh rebuild
|
||||||
|
// egui
|
||||||
|
egui_ctx: egui::Context,
|
||||||
|
egui_state: egui_winit::State,
|
||||||
|
egui_renderer: egui_wgpu::Renderer,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderState {
|
impl RenderState {
|
||||||
@@ -144,14 +144,12 @@ 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);
|
||||||
|
|
||||||
// ── Full bridge scene (Sprint 4) ──────────────────────────────────────
|
// ── Bridge scene (parametric) ─────────────────────────────────────────
|
||||||
// Girder + DeckSlab + Bearing + Abutment
|
let params = SceneParams::default();
|
||||||
#[cfg(feature = "occt")]
|
#[cfg(feature = "occt")]
|
||||||
let mesh = bridge_scene::build_bridge_scene(&OcctKernel)
|
let mesh = build_bridge_scene(&OcctKernel, ¶ms).expect("bridge scene");
|
||||||
.expect("OcctKernel bridge scene");
|
|
||||||
#[cfg(not(feature = "occt"))]
|
#[cfg(not(feature = "occt"))]
|
||||||
let mesh = bridge_scene::build_bridge_scene(&PureRustKernel)
|
let mesh = build_bridge_scene(&PureRustKernel, ¶ms).expect("bridge scene");
|
||||||
.expect("PureRustKernel bridge scene");
|
|
||||||
|
|
||||||
let verts: Vec<Vertex> = mesh.vertices.iter()
|
let verts: Vec<Vertex> = mesh.vertices.iter()
|
||||||
.zip(mesh.normals.iter())
|
.zip(mesh.normals.iter())
|
||||||
@@ -173,7 +171,7 @@ impl RenderState {
|
|||||||
|
|
||||||
// ── Camera ────────────────────────────────────────────────────────────
|
// ── Camera ────────────────────────────────────────────────────────────
|
||||||
// Camera for full bridge scene
|
// Camera for full bridge scene
|
||||||
let (mn, mx) = bridge_scene::scene_extents();
|
let (mn, mx) = scene_extents(¶ms);
|
||||||
let cx = (mn[0] + mx[0]) * 0.5;
|
let cx = (mn[0] + mx[0]) * 0.5;
|
||||||
let cy = (mn[1] + mx[1]) * 0.5;
|
let cy = (mn[1] + mx[1]) * 0.5;
|
||||||
let cz = (mn[2] + mx[2]) * 0.5;
|
let cz = (mn[2] + mx[2]) * 0.5;
|
||||||
@@ -276,7 +274,19 @@ impl RenderState {
|
|||||||
cache: None,
|
cache: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
let (scene_mn, scene_mx) = bridge_scene::scene_extents();
|
let (scene_mn, scene_mx) = scene_extents(¶ms);
|
||||||
|
|
||||||
|
// ── egui ──────────────────────────────────────────────────────────────
|
||||||
|
let egui_ctx = egui::Context::default();
|
||||||
|
let egui_state = egui_winit::State::new(
|
||||||
|
egui_ctx.clone(),
|
||||||
|
egui::ViewportId::ROOT,
|
||||||
|
&*window,
|
||||||
|
None, None, None,
|
||||||
|
);
|
||||||
|
let egui_renderer = egui_wgpu::Renderer::new(
|
||||||
|
&device, format, Some(DEPTH_FORMAT), 1, false,
|
||||||
|
);
|
||||||
|
|
||||||
RenderState {
|
RenderState {
|
||||||
window,
|
window,
|
||||||
@@ -297,6 +307,11 @@ impl RenderState {
|
|||||||
last_mouse: winit::dpi::PhysicalPosition { x: 0.0, y: 0.0 },
|
last_mouse: winit::dpi::PhysicalPosition { x: 0.0, y: 0.0 },
|
||||||
scene_mn,
|
scene_mn,
|
||||||
scene_mx,
|
scene_mx,
|
||||||
|
params,
|
||||||
|
dirty: false,
|
||||||
|
egui_ctx,
|
||||||
|
egui_state,
|
||||||
|
egui_renderer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,6 +339,39 @@ impl RenderState {
|
|||||||
.create_view(&wgpu::TextureViewDescriptor::default())
|
.create_view(&wgpu::TextureViewDescriptor::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Rebuild GPU buffers from current SceneParams. Called when `dirty` is set.
|
||||||
|
fn rebuild_mesh(&mut self) {
|
||||||
|
#[cfg(feature = "occt")]
|
||||||
|
let mesh = build_bridge_scene(&OcctKernel, &self.params);
|
||||||
|
#[cfg(not(feature = "occt"))]
|
||||||
|
let mesh = build_bridge_scene(&PureRustKernel, &self.params);
|
||||||
|
|
||||||
|
if let Ok(mesh) = mesh {
|
||||||
|
let verts: Vec<Vertex> = mesh.vertices.iter()
|
||||||
|
.zip(mesh.normals.iter()).zip(mesh.colors.iter())
|
||||||
|
.map(|((p, n), c)| Vertex { position: *p, normal: *n, base_color: *c })
|
||||||
|
.collect();
|
||||||
|
self.vertex_buffer = self.device.create_buffer_init(
|
||||||
|
&wgpu::util::BufferInitDescriptor {
|
||||||
|
label: Some("mesh vertex buffer"),
|
||||||
|
contents: bytemuck::cast_slice(&verts),
|
||||||
|
usage: wgpu::BufferUsages::VERTEX,
|
||||||
|
});
|
||||||
|
self.index_buffer = self.device.create_buffer_init(
|
||||||
|
&wgpu::util::BufferInitDescriptor {
|
||||||
|
label: Some("mesh index buffer"),
|
||||||
|
contents: bytemuck::cast_slice(&mesh.indices),
|
||||||
|
usage: wgpu::BufferUsages::INDEX,
|
||||||
|
});
|
||||||
|
self.num_indices = mesh.indices.len() as u32;
|
||||||
|
// Update camera extents
|
||||||
|
let (mn, mx) = scene_extents(&self.params);
|
||||||
|
self.scene_mn = mn;
|
||||||
|
self.scene_mx = mx;
|
||||||
|
}
|
||||||
|
self.dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
fn update_camera(&self) {
|
fn update_camera(&self) {
|
||||||
self.queue.write_buffer(
|
self.queue.write_buffer(
|
||||||
&self.camera_buffer,
|
&self.camera_buffer,
|
||||||
@@ -344,6 +392,66 @@ impl RenderState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
|
fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
|
||||||
|
// ── egui UI (use local copies to avoid self-borrow in closure) ───────
|
||||||
|
let raw_input = self.egui_state.take_egui_input(&self.window);
|
||||||
|
let mut p = self.params.clone();
|
||||||
|
let mut dirty = self.dirty;
|
||||||
|
let was_dirty = dirty;
|
||||||
|
let mut apply = false;
|
||||||
|
|
||||||
|
let full_output = self.egui_ctx.run(raw_input, |ctx| {
|
||||||
|
egui::SidePanel::left("properties")
|
||||||
|
.resizable(true)
|
||||||
|
.default_width(230.0)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
ui.heading("교량 속성");
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
let prev = p.span_m;
|
||||||
|
ui.label("경간 (m)");
|
||||||
|
ui.add(egui::Slider::new(&mut p.span_m, 20.0..=80.0).step_by(1.0));
|
||||||
|
if (p.span_m - prev).abs() > 0.001 { dirty = true; }
|
||||||
|
|
||||||
|
let prev = p.girder_count;
|
||||||
|
ui.label("거더 수");
|
||||||
|
ui.add(egui::Slider::new(&mut p.girder_count, 3..=7));
|
||||||
|
if p.girder_count != prev { dirty = true; }
|
||||||
|
|
||||||
|
let prev = p.girder_spacing;
|
||||||
|
ui.label("c/c 간격 (mm)");
|
||||||
|
ui.add(egui::Slider::new(&mut p.girder_spacing, 1_500.0..=4_000.0).step_by(100.0));
|
||||||
|
if (p.girder_spacing - prev).abs() > 1.0 { dirty = true; }
|
||||||
|
|
||||||
|
let prev = p.girder_height;
|
||||||
|
ui.label("거더 높이 (mm)");
|
||||||
|
ui.add(egui::Slider::new(&mut p.girder_height, 1_000.0..=3_000.0).step_by(100.0));
|
||||||
|
if (p.girder_height - prev).abs() > 1.0 { dirty = true; }
|
||||||
|
|
||||||
|
let prev = p.slab_thickness;
|
||||||
|
ui.label("슬래브 두께 (mm)");
|
||||||
|
ui.add(egui::Slider::new(&mut p.slab_thickness, 150.0..=400.0).step_by(10.0));
|
||||||
|
if (p.slab_thickness - prev).abs() > 1.0 { dirty = true; }
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
if dirty {
|
||||||
|
apply = ui.button("▶ 적용").clicked();
|
||||||
|
} else {
|
||||||
|
ui.label("✓ 최신 상태");
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
ui.label("카메라 단축키");
|
||||||
|
ui.small("E: 전체뷰 7: 평면도");
|
||||||
|
ui.small("1: 정면 3: 측면 Home: 아이소");
|
||||||
|
ui.small("가운데버튼: 회전 Shift+가운데: 팬");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
self.egui_state.handle_platform_output(&self.window, full_output.platform_output);
|
||||||
|
self.params = p;
|
||||||
|
self.dirty = dirty;
|
||||||
|
if apply { self.rebuild_mesh(); }
|
||||||
|
|
||||||
|
// ── 3D scene ─────────────────────────────────────────────────────────
|
||||||
let output = self.surface.get_current_texture()?;
|
let output = self.surface.get_current_texture()?;
|
||||||
let view = output.texture.create_view(&Default::default());
|
let view = output.texture.create_view(&Default::default());
|
||||||
let mut enc = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
let mut enc = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||||
@@ -379,7 +487,49 @@ impl RenderState {
|
|||||||
rp.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
|
rp.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
|
||||||
rp.draw_indexed(0..self.num_indices, 0, 0..1);
|
rp.draw_indexed(0..self.num_indices, 0, 0..1);
|
||||||
}
|
}
|
||||||
|
// ── egui render ──────────────────────────────────────────────────────
|
||||||
|
let screen_desc = egui_wgpu::ScreenDescriptor {
|
||||||
|
size_in_pixels: [self.surface_config.width, self.surface_config.height],
|
||||||
|
pixels_per_point: self.window.scale_factor() as f32,
|
||||||
|
};
|
||||||
|
let tris = self.egui_ctx.tessellate(
|
||||||
|
full_output.shapes, screen_desc.pixels_per_point,
|
||||||
|
);
|
||||||
|
for (id, delta) in full_output.textures_delta.set {
|
||||||
|
self.egui_renderer.update_texture(&self.device, &self.queue, id, &delta);
|
||||||
|
}
|
||||||
|
// Submit 3D first
|
||||||
self.queue.submit(std::iter::once(enc.finish()));
|
self.queue.submit(std::iter::once(enc.finish()));
|
||||||
|
|
||||||
|
// egui uses its own encoder (avoids wgpu 22 lifetime issue)
|
||||||
|
let mut egui_enc = self.device.create_command_encoder(
|
||||||
|
&wgpu::CommandEncoderDescriptor { label: Some("egui encoder") },
|
||||||
|
);
|
||||||
|
self.egui_renderer.update_buffers(
|
||||||
|
&self.device, &self.queue, &mut egui_enc, &tris, &screen_desc,
|
||||||
|
);
|
||||||
|
{
|
||||||
|
// wgpu 22: use forget_lifetime() so render pass can be passed to egui renderer
|
||||||
|
let mut rp = egui_enc.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
|
label: Some("egui pass"),
|
||||||
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||||
|
view: &view, resolve_target: None,
|
||||||
|
ops: wgpu::Operations {
|
||||||
|
load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store,
|
||||||
|
},
|
||||||
|
})],
|
||||||
|
depth_stencil_attachment: None,
|
||||||
|
occlusion_query_set: None,
|
||||||
|
timestamp_writes: None,
|
||||||
|
}).forget_lifetime();
|
||||||
|
self.egui_renderer.render(&mut rp, &tris, &screen_desc);
|
||||||
|
}
|
||||||
|
for id in full_output.textures_delta.free {
|
||||||
|
self.egui_renderer.free_texture(&id);
|
||||||
|
}
|
||||||
|
let _ = was_dirty;
|
||||||
|
|
||||||
|
self.queue.submit(std::iter::once(egui_enc.finish()));
|
||||||
output.present();
|
output.present();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -423,6 +573,10 @@ impl ApplicationHandler for CimeryApp {
|
|||||||
let Some(state) = self.state.as_mut() else { return };
|
let Some(state) = self.state.as_mut() else { return };
|
||||||
if state.window.id() != window_id { return; }
|
if state.window.id() != window_id { return; }
|
||||||
|
|
||||||
|
// Forward to egui first; if egui consumes the event, skip camera
|
||||||
|
let egui_resp = state.egui_state.on_window_event(&state.window, &event);
|
||||||
|
if egui_resp.consumed { return; }
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
// ── Exit ──────────────────────────────────────────────────────────
|
// ── Exit ──────────────────────────────────────────────────────────
|
||||||
WindowEvent::CloseRequested => event_loop.exit(),
|
WindowEvent::CloseRequested => event_loop.exit(),
|
||||||
|
|||||||
Reference in New Issue
Block a user