Another project
0

Configure Feed

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

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