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