mod animation;
mod capture;
mod interaction;
mod load_progress;
mod material_variants;
pub use capture::{ViewerCaptureError, ViewerPngError};
use crate::assets::{AssetLoadProgress, AssetPath, Assets};
use crate::controls::{OrbitControlAction, OrbitControls, PointerEvent, TouchEvent};
use crate::diagnostics::{Diagnostic, LookupError, RenderOutcome};
use crate::picking::Hit;
use crate::platform::{PlatformSurface, SurfaceEvent};
use crate::render::{Profile, Quality, RenderMode, Renderer, RendererOptions};
use crate::scene::{CameraKey, Scene, SceneImport, Vec3};
type ViewerPickCallback = Box<dyn FnMut(std::result::Result<Option<Hit>, LookupError>) + 'static>;
#[derive(Debug)]
pub struct FirstRender {
assets: Assets,
scene: Scene,
renderer: Renderer,
import: SceneImport,
outcome: RenderOutcome,
diagnostics: Vec<Diagnostic>,
load_progress_events: Vec<AssetLoadProgress>,
}
#[derive(Debug)]
pub struct HeadlessGltfViewer {
assets: Assets,
scene: Scene,
renderer: Renderer,
import: SceneImport,
load_progress_events: Vec<AssetLoadProgress>,
}
#[derive(Debug, Clone)]
pub struct HeadlessGltfViewerBuilder {
path: AssetPath,
width: u32,
height: u32,
common: ViewerCommonOptions,
}
#[derive(Debug, Clone)]
struct ViewerCommonOptions {
frame_import: bool,
default_light: bool,
default_environment: bool,
environment_path: Option<AssetPath>,
renderer_options: RendererOptions,
}
impl ViewerCommonOptions {
fn new() -> Self {
Self {
frame_import: true,
default_light: false,
default_environment: false,
environment_path: None,
renderer_options: RendererOptions::default(),
}
}
fn with_environment(mut self, path: impl Into<AssetPath>) -> Self {
self.environment_path = Some(path.into());
self.default_environment = false;
self
}
}
pub fn headless_gltf_viewer(path: impl Into<AssetPath>) -> HeadlessGltfViewerBuilder {
HeadlessGltfViewerBuilder {
path: path.into(),
width: 800,
height: 600,
common: ViewerCommonOptions::new(),
}
}
impl FirstRender {
pub fn assets(&self) -> &Assets {
&self.assets
}
pub fn scene(&self) -> &Scene {
&self.scene
}
pub fn renderer(&self) -> &Renderer {
&self.renderer
}
pub fn import(&self) -> &SceneImport {
&self.import
}
pub fn outcome(&self) -> &RenderOutcome {
&self.outcome
}
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
}
impl HeadlessGltfViewerBuilder {
pub const fn size(mut self, width: u32, height: u32) -> Self {
self.width = width;
self.height = height;
self
}
pub const fn with_default_light(mut self) -> Self {
self.common.default_light = true;
self
}
pub const fn with_default_environment(mut self) -> Self {
self.common.default_environment = true;
self
}
pub fn with_environment(mut self, path: impl Into<AssetPath>) -> Self {
self.common = self.common.with_environment(path);
self
}
pub const fn with_profile(mut self, profile: Profile) -> Self {
self.common.renderer_options = self.common.renderer_options.with_profile(profile);
self
}
pub const fn with_quality(mut self, quality: Quality) -> Self {
self.common.renderer_options = self.common.renderer_options.with_quality(quality);
self
}
pub const fn with_render_mode(mut self, render_mode: RenderMode) -> Self {
self.common.renderer_options = self.common.renderer_options.with_render_mode(render_mode);
self
}
pub const fn on_change(self) -> Self {
self.with_render_mode(RenderMode::OnChange)
}
pub const fn without_framing(mut self) -> Self {
self.common.frame_import = false;
self
}
pub async fn build(self) -> crate::Result<HeadlessGltfViewer> {
self.build_with_progress(|_| {}).await
}
pub async fn render(self) -> crate::Result<FirstRender> {
self.render_with_progress(|_| {}).await
}
}
impl HeadlessGltfViewer {
pub fn prepare(&mut self) -> crate::Result<()> {
self.renderer
.prepare_with_assets(&mut self.scene, &self.assets)?;
Ok(())
}
pub fn render_next_frame(&mut self) -> crate::Result<RenderOutcome> {
Ok(self.renderer.render_active(&self.scene)?)
}
pub fn assets(&self) -> &Assets {
&self.assets
}
pub fn scene(&self) -> &Scene {
&self.scene
}
pub fn scene_mut(&mut self) -> &mut Scene {
&mut self.scene
}
pub fn renderer(&self) -> &Renderer {
&self.renderer
}
pub fn renderer_mut(&mut self) -> &mut Renderer {
&mut self.renderer
}
pub fn import(&self) -> &SceneImport {
&self.import
}
pub fn snapshot_rgba8(&self) -> &[u8] {
self.renderer.frame_rgba8()
}
pub fn capabilities(&self) -> &crate::Capabilities {
self.renderer.capabilities()
}
}
pub struct InteractiveGltfViewer {
assets: Assets,
scene: Scene,
renderer: Renderer,
import: SceneImport,
camera: CameraKey,
load_progress_events: Vec<AssetLoadProgress>,
orbit_controls: Option<OrbitControls>,
click_callback: Option<ViewerPickCallback>,
hover_callback: Option<ViewerPickCallback>,
}
impl std::fmt::Debug for InteractiveGltfViewer {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("InteractiveGltfViewer")
.field("assets", &self.assets)
.field("scene", &self.scene)
.field("renderer", &self.renderer)
.field("import", &self.import)
.field("camera", &self.camera)
.field("load_progress_events", &self.load_progress_events)
.field("orbit_controls", &self.orbit_controls)
.field("click_callback_registered", &self.click_callback.is_some())
.field("hover_callback_registered", &self.hover_callback.is_some())
.finish()
}
}
#[derive(Debug)]
pub struct InteractiveGltfViewerBuilder {
path: AssetPath,
surface: PlatformSurface,
orbit_controls: bool,
common: ViewerCommonOptions,
}
pub fn interactive_gltf_viewer(
path: impl Into<AssetPath>,
surface: PlatformSurface,
) -> InteractiveGltfViewerBuilder {
InteractiveGltfViewerBuilder {
path: path.into(),
surface,
orbit_controls: false,
common: ViewerCommonOptions::new(),
}
}
impl InteractiveGltfViewerBuilder {
pub const fn with_default_light(mut self) -> Self {
self.common.default_light = true;
self
}
pub const fn with_default_environment(mut self) -> Self {
self.common.default_environment = true;
self
}
pub fn with_environment(mut self, path: impl Into<AssetPath>) -> Self {
self.common = self.common.with_environment(path);
self
}
pub const fn with_orbit_controls(mut self) -> Self {
self.orbit_controls = true;
self
}
pub const fn with_profile(mut self, profile: Profile) -> Self {
self.common.renderer_options = self.common.renderer_options.with_profile(profile);
self
}
pub const fn with_quality(mut self, quality: Quality) -> Self {
self.common.renderer_options = self.common.renderer_options.with_quality(quality);
self
}
pub const fn with_render_mode(mut self, render_mode: RenderMode) -> Self {
self.common.renderer_options = self.common.renderer_options.with_render_mode(render_mode);
self
}
pub const fn on_change(self) -> Self {
self.with_render_mode(RenderMode::OnChange)
}
pub const fn without_framing(mut self) -> Self {
self.common.frame_import = false;
self
}
#[cfg(not(target_arch = "wasm32"))]
pub fn build(self) -> crate::Result<InteractiveGltfViewer> {
self.build_with_progress(|_| {})
}
pub async fn build_async(self) -> crate::Result<InteractiveGltfViewer> {
self.build_async_with_progress(|_| {}).await
}
}
fn build_orbit_controls(
enabled: bool,
scene: &Scene,
import: &SceneImport,
camera: CameraKey,
) -> Option<OrbitControls> {
if !enabled {
return None;
}
let bounds = import.bounds_world(scene);
let target = bounds.map(|aabb| aabb.center()).unwrap_or(Vec3::ZERO);
let distance = scene
.camera_node(camera)
.and_then(|node| scene.world_transform(node))
.map(|transform| {
let dx = transform.translation.x - target.x;
let dy = transform.translation.y - target.y;
let dz = transform.translation.z - target.z;
(dx * dx + dy * dy + dz * dz).sqrt()
})
.filter(|distance| distance.is_finite() && *distance > 0.0)
.unwrap_or(2.0);
Some(OrbitControls::new(target, distance))
}
impl InteractiveGltfViewer {
pub fn handle_surface_event(&mut self, event: SurfaceEvent) -> crate::Result<()> {
self.renderer.handle_surface_event(event)?;
Ok(())
}
pub fn prepare(&mut self) -> crate::Result<()> {
self.renderer
.prepare_with_assets(&mut self.scene, &self.assets)?;
Ok(())
}
pub fn render_next_frame(&mut self) -> crate::Result<RenderOutcome> {
Ok(self.renderer.render_active(&self.scene)?)
}
pub fn assets(&self) -> &Assets {
&self.assets
}
pub fn scene(&self) -> &Scene {
&self.scene
}
pub fn scene_mut(&mut self) -> &mut Scene {
&mut self.scene
}
pub fn renderer(&self) -> &Renderer {
&self.renderer
}
pub fn renderer_mut(&mut self) -> &mut Renderer {
&mut self.renderer
}
pub fn import(&self) -> &SceneImport {
&self.import
}
pub fn camera(&self) -> CameraKey {
self.camera
}
pub fn orbit_controls(&self) -> Option<&OrbitControls> {
self.orbit_controls.as_ref()
}
pub fn diagnostics(&self) -> Vec<Diagnostic> {
self.renderer.diagnostics().to_vec()
}
pub fn snapshot_rgba8(&self) -> &[u8] {
self.renderer.frame_rgba8()
}
pub fn capabilities(&self) -> &crate::Capabilities {
self.renderer.capabilities()
}
pub fn handle_pointer_event(
&mut self,
event: PointerEvent,
) -> Result<OrbitControlAction, LookupError> {
let Some(orbit_controls) = self.orbit_controls.as_mut() else {
return Ok(OrbitControlAction::None);
};
let action = orbit_controls.handle_pointer(event);
if !matches!(action, OrbitControlAction::None) {
orbit_controls.apply_to_scene(&mut self.scene, self.camera)?;
}
Ok(action)
}
pub fn handle_touch_event(
&mut self,
event: TouchEvent,
) -> Result<OrbitControlAction, LookupError> {
let Some(orbit_controls) = self.orbit_controls.as_mut() else {
return Ok(OrbitControlAction::None);
};
let action = orbit_controls.handle_touch(event);
if !matches!(action, OrbitControlAction::None) {
orbit_controls.apply_to_scene(&mut self.scene, self.camera)?;
}
Ok(action)
}
}
pub async fn first_render_gltf_headless(
path: impl Into<AssetPath>,
width: u32,
height: u32,
) -> crate::Result<FirstRender> {
headless_gltf_viewer(path)
.size(width, height)
.render()
.await
}