use alloc::sync::Arc;
use core::fmt;
use all_is_cubes::character::{Character, Cursor, cursor_raycast};
use all_is_cubes::listen;
use all_is_cubes::math::FreeCoordinate;
use all_is_cubes::space::Space;
use all_is_cubes::universe::{Handle, HandleError, ReadTicket, StrongHandle, Universe};
use crate::camera::{Camera, GraphicsOptions, NdcPoint2, ViewTransform, Viewport};
#[expect(clippy::exhaustive_structs)]
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub struct Layers<T> {
pub world: T,
pub ui: T,
}
impl<T> Layers<T> {
pub fn splat(value: T) -> Self
where
T: Clone,
{
Layers {
world: value.clone(),
ui: value,
}
}
#[cfg(feature = "raytracer")]
pub(crate) fn as_refs(&self) -> Layers<&T> {
Layers {
world: &self.world,
ui: &self.ui,
}
}
#[cfg(feature = "raytracer")]
pub(crate) fn map<U, F: FnMut(T) -> U>(self, mut f: F) -> Layers<U> {
Layers {
world: f(self.world),
ui: f(self.ui),
}
}
#[doc(hidden)] pub fn try_map_ref<U, E>(&self, mut f: impl FnMut(&T) -> Result<U, E>) -> Result<Layers<U>, E> {
Ok(Layers {
world: f(&self.world)?,
ui: f(&self.ui)?,
})
}
}
#[derive(Debug)]
pub struct StandardCameras {
graphics_options: listen::DynSource<Arc<GraphicsOptions>>,
graphics_options_dirty: listen::Flag,
character_source: listen::DynSource<Option<StrongHandle<Character>>>,
character_dirty: listen::Flag,
character: Option<StrongHandle<Character>>,
world_space: listen::Cell<Option<Handle<Space>>>,
ui_source: listen::DynSource<Arc<UiViewState>>,
ui_dirty: listen::Flag,
ui_space: Option<Handle<Space>>,
viewport_source: listen::DynSource<Viewport>,
viewport_dirty: listen::Flag,
cameras: Layers<Camera>,
}
impl StandardCameras {
#[doc(hidden)]
pub fn new(
graphics_options: listen::DynSource<Arc<GraphicsOptions>>,
viewport_source: listen::DynSource<Viewport>,
character_source: listen::DynSource<Option<StrongHandle<Character>>>,
ui_source: listen::DynSource<Arc<UiViewState>>,
) -> Self {
let graphics_options_dirty = listen::Flag::listening(false, &graphics_options);
let viewport_dirty = listen::Flag::listening(false, &viewport_source);
let initial_options: &GraphicsOptions = &graphics_options.get();
let initial_viewport: Viewport = viewport_source.get();
let ui_state = ui_source.get();
Self {
cameras: Layers {
ui: Camera::new((*ui_state.graphics_options).clone(), initial_viewport),
world: Camera::new(initial_options.clone(), initial_viewport),
},
graphics_options,
graphics_options_dirty,
character_dirty: listen::Flag::listening(true, &character_source),
character_source,
character: None, world_space: listen::Cell::new(None),
ui_space: ui_state.space.clone(),
ui_dirty: listen::Flag::listening(true, &ui_source),
ui_source,
viewport_dirty,
viewport_source,
}
}
#[doc(hidden)]
pub fn from_constant_for_test(
graphics_options: GraphicsOptions,
viewport: Viewport,
universe: &Universe,
) -> Self {
let mut new_self = Self::new(
listen::constant(Arc::new(graphics_options)),
listen::constant(viewport),
listen::constant(universe.get_default_character().map(StrongHandle::new)),
listen::constant(Default::default()),
);
new_self.update(Layers {
world: universe.read_ticket(),
ui: ReadTicket::stub(),
});
new_self
}
pub fn update(&mut self, read_tickets: Layers<ReadTicket<'_>>) -> bool {
let mut anything_changed = false;
let options_dirty = self.graphics_options_dirty.get_and_clear();
if options_dirty {
anything_changed = true;
self.cameras.world.set_options((*self.graphics_options.get()).clone());
}
let ui_dirty = self.ui_dirty.get_and_clear();
if ui_dirty || options_dirty {
anything_changed = true;
let UiViewState {
space,
view_transform: ui_transform,
graphics_options: ui_options,
} = if self.graphics_options.get().show_ui {
(*self.ui_source.get()).clone()
} else {
UiViewState::default()
};
self.ui_space = space;
self.cameras.ui.set_options((*ui_options).clone());
self.cameras.ui.set_view_transform(ui_transform);
}
let viewport_dirty = self.viewport_dirty.get_and_clear();
if viewport_dirty {
anything_changed = true;
let viewport: Viewport = self.viewport_source.get();
self.cameras.world.set_viewport(viewport);
self.cameras.ui.set_viewport(viewport);
}
if self.character_dirty.get_and_clear() {
anything_changed = true;
self.character = self.character_source.get();
if self.character.is_none() {
self.cameras.world.set_view_transform(ViewTransform::identity());
}
}
if let Some(character_handle) = &self.character {
match Character::view(character_handle, read_tickets.world) {
Ok((space_handle, view_transform, exposure)) => {
if view_transform != self.cameras.world.view_transform() {
anything_changed = true;
self.cameras.world.set_view_transform(view_transform);
}
if Option::as_ref(&self.world_space.get()) != Some(space_handle) {
anything_changed = true;
self.world_space.set(Some(space_handle.clone()));
}
let old_actual_exposure = self.cameras.world.exposure();
self.cameras.world.set_measured_exposure(exposure);
anything_changed |= self.cameras.world.exposure() != old_actual_exposure;
}
Err(_) => {
}
}
} else {
if self.world_space.get().is_some() {
anything_changed = true;
self.world_space.set(None);
}
}
anything_changed
}
pub fn graphics_options(&self) -> &GraphicsOptions {
self.cameras.world.options()
}
pub fn graphics_options_source(&self) -> listen::DynSource<Arc<GraphicsOptions>> {
self.graphics_options.clone()
}
pub fn cameras(&self) -> &Layers<Camera> {
&self.cameras
}
pub fn character(&self) -> Option<&StrongHandle<Character>> {
self.character.as_ref()
}
pub fn world_space(&self) -> listen::DynSource<Option<Handle<Space>>> {
self.world_space.as_source()
}
pub fn ui_space(&self) -> Option<&Handle<Space>> {
self.ui_space.as_ref()
}
#[cfg(feature = "raytracer")] pub(crate) fn ui_view_source(&self) -> listen::DynSource<Arc<UiViewState>> {
self.ui_source.clone()
}
pub fn viewport(&self) -> Viewport {
self.cameras.world.viewport()
}
pub fn viewport_source(&self) -> listen::DynSource<Viewport> {
self.viewport_source.clone()
}
pub fn project_cursor(
&self,
read_tickets: Layers<ReadTicket<'_>>,
ndc_pos: NdcPoint2,
) -> Result<Option<Cursor>, HandleError> {
if let Some(ui_space_handle) = self.ui_space.as_ref() {
let ray = self.cameras.ui.project_ndc_into_world(ndc_pos);
if let res @ (Ok(Some(_)) | Err(_)) = cursor_raycast(
read_tickets.ui,
ray,
ui_space_handle,
FreeCoordinate::INFINITY,
) {
return res;
}
}
if let Some(character_handle) = self.character.as_ref() {
let ray = self.cameras.world.project_ndc_into_world(ndc_pos);
if let res @ (Ok(Some(_)) | Err(_)) = cursor_raycast(
read_tickets.world,
ray,
character_handle.read(read_tickets.world)?.space(),
6.0,
) {
return res;
}
}
Ok(None)
}
#[must_use]
pub fn clone_unupdated(&self) -> Self {
Self::new(
self.graphics_options.clone(),
self.viewport_source.clone(),
self.character_source.clone(),
self.ui_source.clone(),
)
}
}
impl fmt::Pointer for StandardCameras {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
indoc::indoc! {"\
StandardCameras {{
graphics_options: {graphics_options:p},
viewport_source: {viewport_source:p},
character_source: {character_source:p},
ui_source: {ui_source:p},
}}\
"},
graphics_options = self.graphics_options,
viewport_source = self.viewport_source,
character_source = self.character_source,
ui_source = self.ui_source,
)
}
}
#[derive(Clone, Debug, PartialEq)]
#[expect(clippy::exhaustive_structs)]
pub struct UiViewState {
pub space: Option<Handle<Space>>,
pub view_transform: ViewTransform,
pub graphics_options: Arc<GraphicsOptions>,
}
impl Default for UiViewState {
fn default() -> Self {
Self {
space: Default::default(),
view_transform: ViewTransform::identity(),
graphics_options: Default::default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cameras_follow_character_and_world() {
let character_cell = listen::Cell::new(None);
let mut cameras = StandardCameras::new(
listen::constant(Arc::new(GraphicsOptions::default())),
listen::constant(Viewport::ARBITRARY),
character_cell.as_source(),
listen::constant(Arc::new(UiViewState::default())),
);
cameras.update(Layers::splat(ReadTicket::stub()));
let world_source = cameras.world_space();
let world_flag = listen::Flag::listening(false, &world_source);
assert_eq!(world_source.get().as_ref(), None);
{
let changed = cameras.update(Layers::splat(ReadTicket::stub()));
assert_eq!((changed, world_flag.get_and_clear()), (false, false));
}
let mut universe = Universe::new();
let space_handle = universe.insert_anonymous(Space::empty_positive(1, 1, 1));
let character = universe
.insert(
"character".into(),
Character::spawn_default(universe.read_ticket(), space_handle.clone()).unwrap(),
)
.unwrap();
character_cell.set(Some(StrongHandle::new(character)));
{
assert!(!world_flag.get_and_clear());
let changed = cameras.update(Layers {
world: universe.read_ticket(),
ui: ReadTicket::stub(),
});
assert_eq!((changed, world_flag.get_and_clear()), (true, true));
assert_eq!(world_source.get().as_ref(), Some(&space_handle));
}
{
let changed = cameras.update(Layers {
world: universe.read_ticket(),
ui: ReadTicket::stub(),
});
assert_eq!((changed, world_flag.get_and_clear()), (false, false));
}
}
#[test]
fn cameras_clone() {
let options_cell = listen::Cell::new(Arc::new(GraphicsOptions::default()));
let mut cameras = StandardCameras::new(
options_cell.as_source(),
listen::constant(Viewport::ARBITRARY),
listen::constant(None),
listen::constant(Arc::new(UiViewState::default())),
);
cameras.update(Layers::splat(ReadTicket::stub()));
let mut cameras2 = cameras.clone_unupdated();
cameras2.update(Layers::splat(ReadTicket::stub()));
let default_o = GraphicsOptions::default();
let mut different_o = default_o.clone();
different_o.debug_chunk_boxes = true;
options_cell.set(Arc::new(different_o.clone()));
assert_eq!(cameras.cameras().world.options(), &default_o);
assert_eq!(cameras2.cameras().world.options(), &default_o);
cameras.update(Layers::splat(ReadTicket::stub()));
assert_eq!(cameras.cameras().world.options(), &different_o);
assert_eq!(cameras2.cameras().world.options(), &default_o);
cameras2.update(Layers::splat(ReadTicket::stub()));
assert_eq!(cameras.cameras().world.options(), &different_o);
assert_eq!(cameras2.cameras().world.options(), &different_o);
}
}