use brep_render::controls::{BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT};
use brep_render::engine_state::EngineState;
use brep_render::render::{FrameParams, GpuScene, RenderCore, SAMPLES};
use std::sync::Arc;
use winit::application::ApplicationHandler;
use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::keyboard::{Key, NamedKey};
use winit::window::{Window, WindowId};
struct App {
state: EngineState,
cursor: (f64, f64),
dpr: f32,
dragging: bool,
gpu: Option<Gpu>,
}
struct Gpu {
window: Arc<Window>,
surface: wgpu::Surface<'static>,
config: wgpu::SurfaceConfiguration,
core: RenderCore,
gpu_scene: GpuScene,
}
impl App {
fn init_gpu(&mut self, event_loop: &ActiveEventLoop) {
let window = Arc::new(
event_loop
.create_window(Window::default_attributes().with_title("brep-render"))
.expect("window"),
);
self.dpr = window.scale_factor() as f32;
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle_from_env());
let surface = instance.create_surface(window.clone()).expect("surface");
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: Some(&surface),
force_fallback_adapter: false,
}))
.expect("adapter");
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("brep-render-desktop"),
..Default::default()
}))
.expect("device");
let size = window.inner_size();
let caps = surface.get_capabilities(&adapter);
let format = caps
.formats
.iter()
.copied()
.find(|format| !format.is_srgb())
.unwrap_or(caps.formats[0]);
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format,
width: size.width.max(1),
height: size.height.max(1),
present_mode: wgpu::PresentMode::AutoVsync,
desired_maximum_frame_latency: 2,
alpha_mode: caps.alpha_modes[0],
view_formats: vec![],
};
surface.configure(&device, &config);
let core = RenderCore::new(device, queue, format);
let gpu_scene = GpuScene::default();
self.state.resize(config.width as f64, config.height as f64);
self.state.zoom_to_fit();
self.gpu = Some(Gpu {
window,
surface,
config,
core,
gpu_scene,
});
self.state.dirty = true;
}
fn redraw(&mut self) {
let dpr = self.dpr;
let Some(gpu) = &mut self.gpu else { return };
let state = &mut self.state;
gpu.core.sync_scene(
&mut gpu.gpu_scene,
&state.scene,
&state.settings,
state.settings_generation,
);
let frame = match gpu.surface.get_current_texture() {
wgpu::CurrentSurfaceTexture::Success(frame)
| wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame,
wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => return,
_ => {
gpu.surface.configure(&gpu.core.device, &gpu.config);
match gpu.surface.get_current_texture() {
wgpu::CurrentSurfaceTexture::Success(frame)
| wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame,
other => {
eprintln!("surface unavailable: {other:?}");
return;
}
}
}
};
let view = frame.texture.create_view(&Default::default());
let (camera, overlay) = state.fit_camera_and_overlay();
let params = FrameParams {
camera: &camera,
width: gpu.config.width,
height: gpu.config.height,
dpr,
settings: &state.settings,
emphasis: &state.emphasis,
world_per_pixel: state.camera.world_per_pixel(),
overlay: overlay.as_ref(),
};
gpu.core
.render_to_view(&mut gpu.gpu_scene, &state.scene, ¶ms, &view);
frame.present();
state.dirty = false;
}
fn request_redraw_if_dirty(&self) {
if self.state.dirty {
if let Some(gpu) = &self.gpu {
gpu.window.request_redraw();
}
}
}
fn viewcube_rect(&self) -> Option<(f64, f64, f64, f64)> {
let value: serde_json::Value = serde_json::from_str(&self.state.viewcube_rect_json()).ok()?;
Some((
value["x"].as_f64()?,
value["y"].as_f64()?,
value["w"].as_f64()?,
value["h"].as_f64()?,
))
}
fn viewcube_local(&self, x: f64, y: f64) -> Option<(f64, f64)> {
let (rx, ry, rw, rh) = self.viewcube_rect()?;
if rw > 0.0 && rh > 0.0 && x >= rx && x <= rx + rw && y >= ry && y <= ry + rh {
Some((x - rx, y - ry))
} else {
None
}
}
}
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.gpu.is_none() {
self.init_gpu(event_loop);
self.request_redraw_if_dirty();
}
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
match event {
WindowEvent::CloseRequested => {
event_loop.exit();
return;
}
WindowEvent::Resized(size) => {
if let Some(gpu) = &mut self.gpu {
gpu.config.width = size.width.max(1);
gpu.config.height = size.height.max(1);
gpu.surface.configure(&gpu.core.device, &gpu.config);
self.state.resize(gpu.config.width as f64, gpu.config.height as f64);
}
}
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
self.dpr = scale_factor as f32;
self.state.dirty = true;
}
WindowEvent::MouseInput { state, button, .. } => {
let btn = match button {
MouseButton::Left => BUTTON_LEFT,
MouseButton::Middle => BUTTON_MIDDLE,
MouseButton::Right => BUTTON_RIGHT,
_ => -1,
};
if state == ElementState::Pressed {
if let Some((lx, ly)) = self.viewcube_local(self.cursor.0, self.cursor.1) {
self.state.viewcube_click(lx, ly);
} else {
self.state.pointer_down(self.cursor.0, self.cursor.1, btn);
self.dragging = true;
}
} else {
if self.dragging {
self.state.pointer_up();
self.dragging = false;
}
}
}
WindowEvent::CursorMoved { position, .. } => {
self.cursor = (position.x, position.y);
if self.dragging {
self.state.pointer_move(position.x, position.y);
} else if let Some((lx, ly)) = self.viewcube_local(position.x, position.y) {
self.state.viewcube_hover(lx, ly);
} else {
self.state.viewcube_clear_hover();
}
}
WindowEvent::MouseWheel { delta, .. } => {
let dy = match delta {
MouseScrollDelta::LineDelta(_, y) => -(y as f64) * 100.0,
MouseScrollDelta::PixelDelta(pos) => -pos.y,
};
self.state.wheel(dy, Some([self.cursor.0, self.cursor.1]));
}
WindowEvent::KeyboardInput { event, .. } if event.state == ElementState::Pressed => {
match event.logical_key.as_ref() {
Key::Character("f") | Key::Character("F") => self.state.zoom_to_fit(),
Key::Character("p") | Key::Character("P") => {
self.state.toggle_projection();
}
Key::Character("1") => {
self.state.standard_view("FRONT");
}
Key::Character("2") => {
self.state.standard_view("BACK");
}
Key::Character("3") => {
self.state.standard_view("RIGHT");
}
Key::Character("4") => {
self.state.standard_view("LEFT");
}
Key::Character("5") => {
self.state.standard_view("TOP");
}
Key::Character("6") => {
self.state.standard_view("BOTTOM");
}
Key::Named(NamedKey::Escape) => {
event_loop.exit();
return;
}
_ => {}
}
}
WindowEvent::RedrawRequested => {
if self.state.dirty {
self.redraw();
}
return;
}
_ => return,
}
self.request_redraw_if_dirty();
}
}
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 {
eprintln!("usage: brep-render-desktop <history-request.json>");
std::process::exit(2);
}
let request_json = std::fs::read_to_string(&args[1]).expect("read history request");
let mut state = EngineState::new();
let report_json = state
.run_history_json(&request_json)
.expect("execute history");
if let Ok(report) = serde_json::from_str::<serde_json::Value>(&report_json) {
if let Some(errors) = report["featureErrors"].as_array() {
for error in errors {
if let Some(message) = error.as_str() {
eprintln!("feature error: {message}");
}
}
}
}
state.set_viewcube_enabled(true);
state.zoom_to_fit();
eprintln!(
"scene: {} solid(s), MSAA {SAMPLES}x — drag orbit, right-drag pan, wheel zoom, F fit, P projection, ViewCube in corner",
state.scene.solids().len()
);
let event_loop = EventLoop::new().expect("event loop");
let mut app = App {
state,
cursor: (0.0, 0.0),
dpr: 1.0,
dragging: false,
gpu: None,
};
event_loop.run_app(&mut app).expect("run");
}