Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

1pub mod camera; 2pub mod camera3; 3pub mod diff; 4pub mod gpu; 5pub mod navigate; 6pub mod pick; 7pub mod pipelines; 8pub mod preview; 9pub mod scene; 10pub mod snapshot; 11pub mod surface; 12pub mod tween; 13 14pub use camera::{Camera2, GridSpacing, PixelsPerMm, ViewportExtent, ViewportPx}; 15pub use camera3::{ 16 ViewportPoint, arcball_rotation, clip_from_world, frame_isometric, frame_standard_view, 17 orbit_about_pixel, orbit_about_point, pan_pixels, roll_about_view, world_from_clip, 18 world_on_focal_plane, world_ray, zoom_about_pixel, 19}; 20pub use diff::{PixelDiff, PixelDiffError, PixelDiffReport, PixelDiffThreshold, PixelMismatch}; 21pub use gpu::{BackendTag, Capabilities, Gpu, OffscreenContext}; 22pub use navigate::{DragModifiers, NavGesture, ViewportNavigator}; 23pub use pick::{ 24 EntityKindTag, PickAperture, PickId, PickIdError, PickIndex, PickQuery, PickedItem, Picker, 25}; 26pub use pipelines::{ 27 ArcPipeline, ChromeInstance, ChromePipeline, ChromeTextPipeline, GlyphPipeline, GridPipeline, 28 LinesPipeline, SdfGlyphInstance, TextPipeline, 29}; 30pub(crate) use pipelines::{ 31 Edge3dPipeline, EdgeProjection, EdgeView, FaceFill, HiddenEdges, SolidPipeline, SolidView, 32}; 33pub use preview::{PreviewArc, PreviewCircle, SketchPreview}; 34pub use scene::{ 35 EdgeScene, GenuineEdge, RelationGlyphKind, SceneArc, SceneCircle, SceneDimension, SceneLine, 36 ScenePoint, SceneRelationGlyph, SilhouetteCandidate, SketchScene, SolidScene, 37}; 38pub use snapshot::{ 39 ClearColor, EdgeStyle, GlyphStyle, GridStyle, SnapshotFrame, StrokeStyle, Style, TextStyle, 40 decode_png, encode_png, 41}; 42pub use surface::{SurfaceContext, SurfaceError}; 43pub use tween::CameraTween; 44 45#[allow( 46 clippy::cast_possible_truncation, 47 clippy::cast_precision_loss, 48 reason = "GPU upload narrows f64 geometry to f32 through this single funnel" 49)] 50pub(crate) fn lower_f32(value: f64) -> f32 { 51 value as f32 52} 53 54#[derive(Copy, Clone)] 55pub struct RenderTargets<'a> { 56 pub color: &'a wgpu::TextureView, 57 pub pick: &'a wgpu::TextureView, 58} 59 60impl<'a> RenderTargets<'a> { 61 #[must_use] 62 pub const fn new(color: &'a wgpu::TextureView, pick: &'a wgpu::TextureView) -> Self { 63 Self { color, pick } 64 } 65} 66 67#[derive(Debug, thiserror::Error)] 68pub enum RenderError { 69 #[error("no wgpu adapter matched the offscreen request: {0}")] 70 NoAdapter(#[from] wgpu::RequestAdapterError), 71 #[error("wgpu device request failed: {0}")] 72 Device(#[from] wgpu::RequestDeviceError), 73 #[error("wgpu poll failed: {0}")] 74 Poll(wgpu::PollError), 75 #[error("wgpu buffer map failed: {0}")] 76 Map(wgpu::BufferAsyncError), 77 #[error("wgpu buffer map callback did not fire after poll")] 78 MapMissing, 79 #[error("png encode failed: {0}")] 80 PngEncode(#[from] png::EncodingError), 81 #[error("png decode failed: {0}")] 82 PngDecode(#[from] png::DecodingError), 83 #[error("png format unsupported: color={color_type:?}, depth={bit_depth:?}, require rgba8")] 84 PngFormat { 85 color_type: png::ColorType, 86 bit_depth: png::BitDepth, 87 }, 88 #[error("viewport dimension is zero")] 89 ZeroExtent, 90 #[error("pick id construction failed: {0}")] 91 PickId(#[from] PickIdError), 92 #[error("pick query {query} outside viewport {extent}")] 93 PickOutOfBounds { 94 query: PickQuery, 95 extent: ViewportExtent, 96 }, 97 #[error("camera lowering failed: {0}")] 98 Camera(#[from] bone_types::TypesError), 99} 100 101pub type Result<T, E = RenderError> = core::result::Result<T, E>; 102 103#[derive(Debug)] 104pub struct SketchRenderer { 105 grid: GridPipeline, 106 arcs: ArcPipeline, 107 lines: LinesPipeline, 108 glyphs: GlyphPipeline, 109 text: TextPipeline, 110} 111 112impl SketchRenderer { 113 #[must_use] 114 pub fn new(gpu: &Gpu, color_format: wgpu::TextureFormat) -> Self { 115 Self { 116 grid: GridPipeline::new(gpu, color_format), 117 arcs: ArcPipeline::new(gpu, color_format), 118 lines: LinesPipeline::new(gpu, color_format), 119 glyphs: GlyphPipeline::new(gpu, color_format), 120 text: TextPipeline::new(gpu, color_format), 121 } 122 } 123 124 pub fn prepare(&mut self, scene: &SketchScene, style: &Style) { 125 self.text.prepare(scene, style); 126 } 127 128 #[must_use] 129 pub fn text_cache_len(&self) -> usize { 130 self.text.cache_len() 131 } 132 133 pub fn encode_passes( 134 &self, 135 encoder: &mut wgpu::CommandEncoder, 136 targets: RenderTargets<'_>, 137 scene: &SketchScene, 138 preview: &SketchPreview, 139 camera: Camera2, 140 style: &Style, 141 ) { 142 self.grid.draw(encoder, targets.color, camera, style); 143 gpu::clear_pick_attachment(encoder, targets.pick); 144 self.arcs 145 .draw(encoder, targets, camera, style, scene, preview); 146 self.lines 147 .draw(encoder, targets, camera, style, scene, preview); 148 self.glyphs.draw(encoder, targets, camera, style, scene); 149 self.text.draw(encoder, targets, camera, style, scene); 150 } 151 152 pub fn render( 153 &mut self, 154 ctx: &OffscreenContext, 155 scene: &SketchScene, 156 camera: Camera2, 157 style: &Style, 158 ) -> Result<SnapshotFrame> { 159 self.render_with_preview(ctx, scene, &SketchPreview::empty(), camera, style) 160 } 161 162 pub fn render_with_preview( 163 &mut self, 164 ctx: &OffscreenContext, 165 scene: &SketchScene, 166 preview: &SketchPreview, 167 camera: Camera2, 168 style: &Style, 169 ) -> Result<SnapshotFrame> { 170 debug_assert_eq!( 171 camera.extent(), 172 ctx.extent(), 173 "camera extent must match offscreen context extent", 174 ); 175 self.prepare(scene, style); 176 ctx.render(|encoder, color_view, pick_view| { 177 self.encode_passes( 178 encoder, 179 RenderTargets::new(color_view, pick_view), 180 scene, 181 preview, 182 camera, 183 style, 184 ); 185 }) 186 } 187} 188 189#[derive(Debug)] 190pub struct SolidRenderer { 191 solid: SolidPipeline, 192 edges: Edge3dPipeline, 193 depth: Option<DepthTarget>, 194 shading: bone_types::ShadingModel, 195} 196 197#[derive(Debug)] 198struct DepthTarget { 199 extent: ViewportExtent, 200 texture: wgpu::Texture, 201} 202 203impl SolidRenderer { 204 #[must_use] 205 pub fn new(gpu: &Gpu, color_format: wgpu::TextureFormat) -> Self { 206 Self { 207 solid: SolidPipeline::new(gpu, color_format), 208 edges: Edge3dPipeline::new(gpu, color_format), 209 depth: None, 210 shading: bone_types::ShadingModel::Gouraud, 211 } 212 } 213 214 #[must_use] 215 pub const fn with_shading_model(mut self, shading: bone_types::ShadingModel) -> Self { 216 self.shading = shading; 217 self 218 } 219 220 pub fn render( 221 &mut self, 222 ctx: &OffscreenContext, 223 scene: &SolidScene, 224 camera: bone_types::Camera3, 225 style: &Style, 226 ) -> Result<SnapshotFrame> { 227 self.render_display( 228 ctx, 229 scene, 230 &EdgeScene::empty(), 231 camera, 232 style, 233 bone_types::DisplayMode::ShadedNoEdges, 234 ) 235 } 236 237 pub fn render_display( 238 &mut self, 239 ctx: &OffscreenContext, 240 scene: &SolidScene, 241 edges: &EdgeScene, 242 camera: bone_types::Camera3, 243 style: &Style, 244 mode: bone_types::DisplayMode, 245 ) -> Result<SnapshotFrame> { 246 let plan = DisplayPlan::for_mode(mode); 247 let extent = ctx.extent(); 248 let clip_from_world = camera3::clip_from_world(camera, extent)?; 249 let (ex, ey, ez) = camera.eye().coords_mm(); 250 let eye_world = [lower_f32(ex), lower_f32(ey), lower_f32(ez)]; 251 if !matches!(&self.depth, Some(target) if target.extent == extent) { 252 self.depth = Some(DepthTarget { 253 extent, 254 texture: depth_texture(ctx.gpu().device(), extent), 255 }); 256 } 257 let Some(target) = &self.depth else { 258 unreachable!("depth target populated above"); 259 }; 260 let depth_view = target 261 .texture 262 .create_view(&wgpu::TextureViewDescriptor::default()); 263 let solid = &self.solid; 264 let edge_pipeline = &self.edges; 265 let shading = self.shading; 266 let edge_view = EdgeView { 267 clip_from_world, 268 projection: EdgeProjection::from_camera(camera), 269 viewport_px: [ 270 lower_f32(f64::from(extent.width().value())), 271 lower_f32(f64::from(extent.height().value())), 272 ], 273 crease_threshold_rad: CREASE_THRESHOLD_RAD, 274 dash_period_px: HIDDEN_DASH_PERIOD_PX, 275 dash_on_ratio: HIDDEN_DASH_ON_RATIO, 276 edge_color: style.edges().visible().to_rgba_array(), 277 hidden_color: style.edges().hidden().to_rgba_array(), 278 }; 279 ctx.render(|encoder, color_view, pick_view| { 280 let targets = RenderTargets::new(color_view, pick_view); 281 match plan.solid { 282 Some(fill) => solid.draw( 283 encoder, 284 targets, 285 &depth_view, 286 scene, 287 SolidView { 288 clip_from_world, 289 eye_world, 290 shading, 291 fill, 292 }, 293 style, 294 ), 295 None => clear_solid_targets(encoder, targets, &depth_view, style), 296 } 297 if plan.edges && !edges.is_empty() { 298 edge_pipeline.draw(encoder, targets, &depth_view, edges, edge_view, plan.hidden); 299 } 300 }) 301 } 302} 303 304struct DisplayPlan { 305 solid: Option<FaceFill>, 306 edges: bool, 307 hidden: HiddenEdges, 308} 309 310impl DisplayPlan { 311 const fn for_mode(mode: bone_types::DisplayMode) -> Self { 312 use bone_types::DisplayMode; 313 match mode { 314 DisplayMode::Wireframe => Self { 315 solid: None, 316 edges: true, 317 hidden: HiddenEdges::Omit, 318 }, 319 DisplayMode::HiddenLineRemoved => Self { 320 solid: Some(FaceFill::Occluder), 321 edges: true, 322 hidden: HiddenEdges::Omit, 323 }, 324 DisplayMode::HiddenLineGray => Self { 325 solid: Some(FaceFill::Occluder), 326 edges: true, 327 hidden: HiddenEdges::Dashed, 328 }, 329 DisplayMode::ShadedWithEdges => Self { 330 solid: Some(FaceFill::Shaded), 331 edges: true, 332 hidden: HiddenEdges::Omit, 333 }, 334 DisplayMode::ShadedNoEdges => Self { 335 solid: Some(FaceFill::Shaded), 336 edges: false, 337 hidden: HiddenEdges::Omit, 338 }, 339 } 340 } 341} 342 343pub(crate) fn solid_clear_color_attachments<'a>( 344 targets: RenderTargets<'a>, 345 style: &Style, 346) -> [Option<wgpu::RenderPassColorAttachment<'a>>; 2] { 347 [ 348 Some(wgpu::RenderPassColorAttachment { 349 view: targets.color, 350 resolve_target: None, 351 depth_slice: None, 352 ops: wgpu::Operations { 353 load: wgpu::LoadOp::Clear(style.background().into()), 354 store: wgpu::StoreOp::Store, 355 }, 356 }), 357 Some(wgpu::RenderPassColorAttachment { 358 view: targets.pick, 359 resolve_target: None, 360 depth_slice: None, 361 ops: wgpu::Operations { 362 load: wgpu::LoadOp::Clear(gpu::pick_clear_color()), 363 store: wgpu::StoreOp::Store, 364 }, 365 }), 366 ] 367} 368 369pub(crate) fn solid_clear_depth_attachment( 370 depth_view: &wgpu::TextureView, 371) -> wgpu::RenderPassDepthStencilAttachment<'_> { 372 wgpu::RenderPassDepthStencilAttachment { 373 view: depth_view, 374 depth_ops: Some(wgpu::Operations { 375 load: wgpu::LoadOp::Clear(1.0), 376 store: wgpu::StoreOp::Store, 377 }), 378 stencil_ops: None, 379 } 380} 381 382fn clear_solid_targets( 383 encoder: &mut wgpu::CommandEncoder, 384 targets: RenderTargets<'_>, 385 depth_view: &wgpu::TextureView, 386 style: &Style, 387) { 388 let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { 389 label: Some("bone-render:solid-clear"), 390 color_attachments: &solid_clear_color_attachments(targets, style), 391 depth_stencil_attachment: Some(solid_clear_depth_attachment(depth_view)), 392 timestamp_writes: None, 393 occlusion_query_set: None, 394 multiview_mask: None, 395 }); 396} 397 398const CREASE_THRESHOLD_RAD: f64 = core::f64::consts::FRAC_PI_6; 399const HIDDEN_DASH_PERIOD_PX: f32 = 6.0; 400const HIDDEN_DASH_ON_RATIO: f32 = 0.5; 401 402pub(crate) fn depth_texture(device: &wgpu::Device, extent: ViewportExtent) -> wgpu::Texture { 403 device.create_texture(&wgpu::TextureDescriptor { 404 label: Some("bone-render:solid-depth"), 405 size: wgpu::Extent3d { 406 width: extent.width().value(), 407 height: extent.height().value(), 408 depth_or_array_layers: 1, 409 }, 410 mip_level_count: 1, 411 sample_count: 1, 412 dimension: wgpu::TextureDimension::D2, 413 format: pipelines::solid::DEPTH_FORMAT, 414 usage: wgpu::TextureUsages::RENDER_ATTACHMENT, 415 view_formats: &[], 416 }) 417}