Another project
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}