use crate::{
MainWindow, SettingsModel, SolidPreviewModel, ViewportModel,
bridge::render_thread::{RenderContext, load_env_map},
gui::{
env_map_status_text, show_toast,
solid_preview::preview_state::{CameraPose, SolidPreviewState},
},
settings::SettingsPersister,
};
use slint::{ComponentHandle, SharedString};
use std::sync::{Arc, Mutex};
fn resubmit_at_current_pose(
ui: &MainWindow,
ctx: &RenderContext,
preview_state: &SolidPreviewState,
) {
if ctx.active_planes.is_empty() {
return;
}
let planes = ctx
.active_planes
.iter()
.map(|p| (glam::Vec3::from(p.normal), -p.d))
.collect();
let size = (
ui.global::<SolidPreviewModel>().get_viewport_width() as u32,
ui.global::<SolidPreviewModel>().get_viewport_height() as u32,
);
let view_mode = ui.global::<SolidPreviewModel>().get_view_mode() as u8;
preview_state.request_redraw_with_gear(
planes,
CameraPose {
yaw: ctx.yaw,
pitch: ctx.pitch,
distance: ctx.distance,
},
size,
view_mode,
ctx.design_gear,
);
}
pub(in crate::gui) fn setup_camera_and_lighting_callbacks(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
settings_store: &Arc<SettingsPersister>,
preview_state: &Arc<SolidPreviewState>,
) {
let render_ctx_orbit = render_ctx.clone();
let settings_store_orbit = settings_store.clone();
let preview_state_orbit = Arc::clone(preview_state);
let ui_weak_orbit = ui.as_weak();
ui.global::<ViewportModel>()
.on_camera_orbit(move |dx: f32, dy: f32| {
let Some(ui) = ui_weak_orbit.upgrade() else {
return;
};
let (yaw, pitch) = {
let mut ctx = render_ctx_orbit.lock().unwrap();
ctx.yaw = dx.mul_add(-0.008, ctx.yaw);
ctx.pitch = dy.mul_add(0.008, ctx.pitch).clamp(-1.48, 1.48);
ctx.dirty = true;
resubmit_at_current_pose(&ui, &ctx, &preview_state_orbit);
(ctx.yaw, ctx.pitch)
};
settings_store_orbit.update(|s| {
s.settings.camera_yaw = yaw;
s.settings.camera_pitch = pitch;
});
});
let render_ctx_zoom = render_ctx.clone();
let settings_store_zoom = settings_store.clone();
let preview_state_zoom = Arc::clone(preview_state);
let ui_weak_zoom = ui.as_weak();
ui.global::<ViewportModel>()
.on_camera_zoom(move |delta: f32| {
let Some(ui) = ui_weak_zoom.upgrade() else {
return;
};
let distance = {
let mut ctx = render_ctx_zoom.lock().unwrap();
ctx.distance = delta.mul_add(-0.002, ctx.distance).clamp(1.2, 8.0);
ctx.dirty = true;
resubmit_at_current_pose(&ui, &ctx, &preview_state_zoom);
ctx.distance
};
settings_store_zoom.update(|s| s.settings.camera_distance = distance);
});
let render_ctx_light = render_ctx.clone();
let settings_store_light = settings_store.clone();
ui.global::<ViewportModel>()
.on_light_move(move |dx: f32, dy: f32| {
let (light_yaw, light_pitch) = {
let mut ctx = render_ctx_light.lock().unwrap();
ctx.light_yaw = dx.mul_add(0.01, ctx.light_yaw);
ctx.light_pitch = dy.mul_add(0.01, ctx.light_pitch).clamp(0.15, 1.55);
ctx.dirty = true;
(ctx.light_yaw, ctx.light_pitch)
};
settings_store_light.update(|s| {
s.settings.light_yaw_deg = light_yaw.to_degrees();
s.settings.light_pitch_deg = light_pitch.to_degrees();
});
});
let render_ctx_light_pos = render_ctx.clone();
let settings_store_light_pos = settings_store.clone();
ui.global::<ViewportModel>()
.on_light_pos_changed(move |yaw_deg: f32, pitch_deg: f32| {
{
let mut ctx = render_ctx_light_pos.lock().unwrap();
ctx.light_yaw = yaw_deg.to_radians();
ctx.light_pitch = pitch_deg.to_radians().clamp(0.15, 1.55);
ctx.dirty = true;
}
settings_store_light_pos.update(|s| {
s.settings.light_yaw_deg = yaw_deg;
s.settings.light_pitch_deg = pitch_deg;
});
});
let render_ctx_reset = render_ctx.clone();
let settings_store_reset = settings_store.clone();
ui.global::<ViewportModel>().on_reset_camera(move || {
{
let mut ctx = render_ctx_reset.lock().unwrap();
ctx.yaw = 0.60;
ctx.pitch = 0.45;
ctx.distance = 2.4;
ctx.light_yaw = 0.85;
ctx.light_pitch = 0.95;
ctx.dirty = true;
}
settings_store_reset.update(|s| {
s.settings.camera_yaw = 0.60;
s.settings.camera_pitch = 0.45;
s.settings.camera_distance = 2.4;
s.settings.light_yaw_deg = 0.85_f32.to_degrees();
s.settings.light_pitch_deg = 0.95_f32.to_degrees();
});
});
}
pub(in crate::gui) fn setup_environment_map_callbacks(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
settings_store: &Arc<SettingsPersister>,
) {
let render_ctx_load = render_ctx.clone();
let settings_store_load = settings_store.clone();
let ui_weak_load = ui.as_weak();
ui.global::<SettingsModel>()
.on_load_env_map(move |raw_path: SharedString| {
let Some(ui) = ui_weak_load.upgrade() else {
return;
};
let path = raw_path.to_string();
match load_env_map(&path) {
Ok(map) => {
let status = env_map_status_text(&map, &path);
{
let mut ctx = render_ctx_load.lock().unwrap();
ctx.env_map = Some(map);
ctx.dirty = true;
}
settings_store_load.update(|s| s.settings.env_map_path.clone_from(&path));
ui.global::<SettingsModel>()
.set_env_map_status(status.clone().into());
ui.global::<SettingsModel>().set_env_map_loaded(true);
show_toast(
&ui,
&format!("{status}. Rendering on CPU: the GPU backend has no HDR support."),
"info",
);
}
Err(err) => {
show_toast(
&ui,
&format!("Could not load HDR environment: {err}"),
"error",
);
}
}
});
let render_ctx_clear = render_ctx.clone();
let settings_store_clear = settings_store.clone();
let ui_weak_clear = ui.as_weak();
ui.global::<SettingsModel>().on_clear_env_map(move || {
let Some(ui) = ui_weak_clear.upgrade() else {
return;
};
{
let mut ctx = render_ctx_clear.lock().unwrap();
ctx.env_map = None;
ctx.dirty = true;
}
settings_store_clear.update(|s| s.settings.env_map_path.clear());
ui.global::<SettingsModel>()
.set_env_map_status(String::new().into());
ui.global::<SettingsModel>().set_env_map_loaded(false);
show_toast(
&ui,
"Cleared HDR environment; back to the studio rig.",
"info",
);
});
ui.global::<SettingsModel>()
.on_pick_hdr_file(|current: SharedString| {
let mut dialog = rfd::FileDialog::new().add_filter("Radiance HDR", &["hdr"]);
if let Some(dir) = crate::gui::starting_dir_from_picker_field(current.as_str()) {
dialog = dialog.set_directory(dir);
}
dialog
.pick_file()
.map_or(current, |path| path.display().to_string().into())
});
}