Skip to main content

brep_render/render/
frame.rs

1use super::*;
2
3impl RenderCore {
4    /// Render one frame into `resolve_view` (a single-sample view of the
5    /// core's format). This is the whole engine core; every presentation shell
6    /// funnels through it.
7    pub fn render_to_view(
8        &mut self,
9        gpu_scene: &mut GpuScene,
10        scene: &RenderScene,
11        params: &FrameParams,
12        resolve_view: &wgpu::TextureView,
13    ) {
14        self.render_to_view_with_context(gpu_scene, scene, params, resolve_view, None);
15    }
16
17    /// Draw optional, non-pickable assembly context after the opaque work part.
18    pub fn render_to_view_with_context(
19        &mut self,
20        gpu_scene: &mut GpuScene,
21        scene: &RenderScene,
22        params: &FrameParams,
23        resolve_view: &wgpu::TextureView,
24        context: Option<(&GpuScene, &RenderScene)>,
25    ) {
26        let width = params.width.max(1);
27        let height = params.height.max(1);
28        self.ensure_targets(width, height);
29        self.write_global_styles(params.settings);
30
31        let globals = Globals {
32            view_proj: params.camera.view_proj,
33            viewport: [width as f32, height as f32, params.dpr.max(1e-3), 0.0],
34            forward: [
35                params.camera.forward[0],
36                params.camera.forward[1],
37                params.camera.forward[2],
38                0.0,
39            ],
40        };
41        self.queue
42            .write_buffer(&self.globals_buf, 0, bytemuck::bytes_of(&globals));
43
44        // World-axis helper (R20): three world-axis segments sized in CSS px.
45        let axis_len = params.settings.axis_length_px as f64 * params.world_per_pixel;
46        let draw_axes = params.settings.axis_length_px > 0.0 && axis_len.is_finite() && axis_len > 0.0;
47        if draw_axes {
48            let l = axis_len as f32;
49            let segments = [
50                EdgeInstance { p0: [0.0; 3], p1: [l, 0.0, 0.0] },
51                EdgeInstance { p0: [0.0; 3], p1: [0.0, l, 0.0] },
52                EdgeInstance { p0: [0.0; 3], p1: [0.0, 0.0, l] },
53            ];
54            match &gpu_scene.axis_buf {
55                Some(buf) => self.queue.write_buffer(buf, 0, bytemuck::cast_slice(&segments)),
56                None => {
57                    gpu_scene.axis_buf = Some(self.device.create_buffer_init(
58                        &wgpu::util::BufferInitDescriptor {
59                            label: Some("world axes"),
60                            contents: bytemuck::cast_slice(&segments),
61                            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
62                        },
63                    ));
64                }
65            }
66        }
67
68        // Refresh emphasis boundary buffers.
69        let has_emphasis = !params.emphasis.is_empty();
70        // A solid a transparency clip made see-through draws in the blended pass.
71        let translucent = |solid: &SolidDisplay| solid.opacity.is_some_and(|opacity| opacity < 1.0);
72        for solid in scene.solids() {
73            if let Some(gpu_solid) = gpu_scene.solids.get_mut(&solid.name) {
74                self.sync_boundary(gpu_solid, solid, params.emphasis);
75            }
76        }
77
78        // A Line art instructions step draws on the step pictures' ground.
79        let bg = match &scene.line_art {
80            Some(_) => crate::thumbnail::line_art::BACKGROUND,
81            None => params.settings.background,
82        };
83        let targets = self.targets.as_ref().expect("targets ensured");
84        let mut encoder = self
85            .device
86            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
87                label: Some("brep-render frame"),
88            });
89        {
90            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
91                label: Some("scene"),
92                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
93                    view: &targets.msaa_view,
94                    depth_slice: None,
95                    resolve_target: Some(resolve_view),
96                    ops: wgpu::Operations {
97                        load: wgpu::LoadOp::Clear(wgpu::Color {
98                            r: bg[0] as f64,
99                            g: bg[1] as f64,
100                            b: bg[2] as f64,
101                            a: 1.0,
102                        }),
103                        store: wgpu::StoreOp::Store,
104                    },
105                })],
106                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
107                    view: &targets.depth_view,
108                    depth_ops: Some(wgpu::Operations {
109                        load: wgpu::LoadOp::Clear(1.0),
110                        store: wgpu::StoreOp::Discard,
111                    }),
112                    stencil_ops: None,
113                }),
114                timestamp_writes: None,
115                occlusion_query_set: None,
116                multiview_mask: None,
117            });
118            pass.set_bind_group(0, &self.globals_bind, &[]);
119            // The solid pipelines' group 2: a solid's pose, set beside each
120            // solid's buffers; identity for anything else they draw.
121            pass.set_bind_group(2, &self.identity_model.bind, &[0]);
122
123            let visible_solids: Vec<(&SolidDisplay, &GpuSolid)> = gpu_scene
124                .order
125                .iter()
126                .filter_map(|name| {
127                    let solid = scene.solid(name)?;
128                    if !solid.visible {
129                        return None;
130                    }
131                    Some((solid, gpu_scene.solids.get(name)?))
132                })
133                .collect();
134
135            // 1a. Wireframe view: draw each solid's tessellated triangle mesh as
136            //     a line list (base color, no fill), so the face triangles read
137            //     as a wireframe. Picking is CPU ray-based (pick.rs), unaffected.
138            //     A HIDDEN face skips its triangles' wire segments too, exactly as
139            //     the shaded pass masks its triangle ranges — otherwise hiding a
140            //     face (or the whole Faces group) would be a no-op in wireframe view.
141            if params.settings.wireframe && params.settings.show_faces {
142                pass.set_pipeline(&self.wire_pipeline);
143                for (solid, gpu_solid) in &visible_solids {
144                    if gpu_solid.wire_index_count == 0 {
145                        continue;
146                    }
147                    bind_pose(&mut pass, gpu_scene.models.as_ref(), &self.identity_model, gpu_solid.model_slot);
148                    pass.set_vertex_buffer(0, gpu_solid.vertex_buf.slice(..));
149                    pass.set_index_buffer(
150                        gpu_solid.wire_index_buf.slice(..),
151                        wgpu::IndexFormat::Uint32,
152                    );
153                    pass.set_bind_group(1, &gpu_solid.base_style.bind, &[]);
154                    // Fast path: no hidden face → one whole-buffer wire draw.
155                    if !solid.visibility.any_face_hidden() {
156                        pass.draw_indexed(0..gpu_solid.wire_index_count, 0, 0..1);
157                        continue;
158                    }
159                    // Otherwise coalesce contiguous VISIBLE faces' wire ranges and
160                    // skip the hidden ones. The wire buffer is a LINE LIST whose
161                    // segments are grouped by face and de-duplicated inside each
162                    // face (see `scene_sync::upload_solid`), so a face's slice is
163                    // whatever that grouping produced — read it off the face's
164                    // `wire_first`/`wire_count` rather than deriving it from the
165                    // triangle range, which no longer predicts it.
166                    let mut run: Option<(u32, u32)> = None; // first, count
167                    let flush = |pass: &mut wgpu::RenderPass, run: &mut Option<(u32, u32)>| {
168                        if let Some((first, count)) = run.take() {
169                            if count > 0 {
170                                pass.draw_indexed(first..first + count, 0, 0..1);
171                            }
172                        }
173                    };
174                    for (index, _face) in solid.faces.iter().enumerate() {
175                        let range = &gpu_solid.faces[index];
176                        if range.wire_count == 0 {
177                            continue;
178                        }
179                        let first = range.wire_first;
180                        let count = range.wire_count;
181                        if !solid.visibility.is_face_visible(index) {
182                            flush(&mut pass, &mut run);
183                            continue;
184                        }
185                        match &mut run {
186                            Some((run_first, run_count)) if *run_first + *run_count == first => {
187                                *run_count += count;
188                            }
189                            _ => {
190                                flush(&mut pass, &mut run);
191                                run = Some((first, count));
192                            }
193                        }
194                    }
195                    flush(&mut pass, &mut run);
196                }
197            }
198
199            // 1. Shaded faces, coalesced into index-range runs per emphasis
200            //    state (base runs merge back into whole-solid draws). Skipped in
201            //    wireframe mode (1a draws the triangle wireframe instead). Picking
202            //    is CPU ray-based (pick.rs) and the overlay pass is separate.
203            if !params.settings.wireframe && params.settings.show_faces {
204                pass.set_pipeline(&self.mesh_pipeline);
205                for (solid, gpu_solid) in &visible_solids {
206                    if solid.mesh.indices.is_empty() || translucent(solid) {
207                        continue;
208                    }
209                    bind_pose(&mut pass, gpu_scene.models.as_ref(), &self.identity_model, gpu_solid.model_slot);
210                    pass.set_vertex_buffer(0, gpu_solid.vertex_buf.slice(..));
211                    pass.set_index_buffer(gpu_solid.index_buf.slice(..), wgpu::IndexFormat::Uint32);
212                    // A Line art step: one flat fill per tier, whatever the
213                    // model's colours.
214                    if let Some(look) = &scene.line_art {
215                        let style = if look.placed.contains(&solid.name) { &self.styles.line_art_placed_face } else { &self.styles.line_art_adding_face };
216                        if !has_emphasis {
217                            pass.set_bind_group(1, &style.bind, &[]);
218                            draw_visible_faces(&mut pass, solid, gpu_solid);
219                            continue;
220                        }
221                        // A picked or hovered face still shows, in the
222                        // emphasis colour in place of the flat fill.
223                        for (index, face) in solid.faces.iter().enumerate() {
224                            let range = &gpu_solid.faces[index];
225                            if range.index_count == 0 || !solid.visibility.is_face_visible(index) {
226                                continue;
227                            }
228                            let bind = match params.emphasis.face_state(&solid.name, &face.name) {
229                                EmphasisState::Base => &style.bind,
230                                EmphasisState::Selected => &self.styles.face_selected.bind,
231                                EmphasisState::Hovered => &self.styles.face_hovered.bind,
232                            };
233                            pass.set_bind_group(1, bind, &[]);
234                            pass.draw_indexed(range.first_index..range.first_index + range.index_count, 0, 0..1);
235                        }
236                        continue;
237                    }
238                    // Fast path: no emphasis AND no hidden faces → one whole-mesh
239                    // draw. When any face is hidden we fall to the per-face loop
240                    // below, which coalesces contiguous VISIBLE faces and skips
241                    // the hidden ones' triangle ranges entirely.
242                    // The whole-mesh fast path also needs every face to share
243                    // the solid's colour — a solid with a per-face palette has
244                    // to walk its faces to bind the right style per run.
245                    let any_face_hidden = solid.visibility.any_face_hidden();
246                    let any_face_colored = !gpu_solid.face_styles.is_empty();
247                    if !has_emphasis && !any_face_hidden && !any_face_colored {
248                        pass.set_bind_group(1, &gpu_solid.base_style.bind, &[]);
249                        pass.draw_indexed(0..solid.mesh.indices.len() as u32, 0, 0..1);
250                        continue;
251                    }
252                    // The run key is (emphasis state, face-colour slot). A
253                    // selected or hovered face draws in the emphasis colour
254                    // whatever colour the model gave it, so its slot is
255                    // normalized away — two adjacent selected faces of
256                    // different colours still coalesce into one draw.
257                    let style_for = |state: EmphasisState, slot: Option<u32>| match state {
258                        EmphasisState::Base => match slot.and_then(|s| gpu_solid.face_styles.get(s as usize)) {
259                            Some(style) => &style.bind,
260                            None => &gpu_solid.base_style.bind,
261                        },
262                        EmphasisState::Selected => &self.styles.face_selected.bind,
263                        EmphasisState::Hovered => &self.styles.face_hovered.bind,
264                    };
265                    type FaceRun = (EmphasisState, Option<u32>, u32, u32); // state, slot, first, count
266                    let mut run: Option<FaceRun> = None;
267                    let flush = |pass: &mut wgpu::RenderPass, run: &mut Option<FaceRun>| {
268                        if let Some((state, slot, first, count)) = run.take() {
269                            if count > 0 {
270                                pass.set_bind_group(1, style_for(state, slot), &[]);
271                                pass.draw_indexed(first..first + count, 0, 0..1);
272                            }
273                        }
274                    };
275                    for (index, face) in solid.faces.iter().enumerate() {
276                        let range = &gpu_solid.faces[index];
277                        if range.index_count == 0 {
278                            continue;
279                        }
280                        // Hidden face: skip its triangles and break the run so
281                        // the surviving neighbours don't coalesce across the gap.
282                        if !solid.visibility.is_face_visible(index) {
283                            flush(&mut pass, &mut run);
284                            continue;
285                        }
286                        let state = if has_emphasis {
287                            params.emphasis.face_state(&solid.name, &face.name)
288                        } else {
289                            EmphasisState::Base
290                        };
291                        let slot = if state == EmphasisState::Base {
292                            range.style
293                        } else {
294                            None
295                        };
296                        match &mut run {
297                            Some((run_state, run_slot, first, count))
298                                if *run_state == state
299                                    && *run_slot == slot
300                                    && *first + *count == range.first_index =>
301                            {
302                                *count += range.index_count;
303                            }
304                            _ => {
305                                flush(&mut pass, &mut run);
306                                run = Some((state, slot, range.first_index, range.index_count));
307                            }
308                        }
309                    }
310                    flush(&mut pass, &mut run);
311                }
312            }
313
314            // 1b. A Line art step's outlines: each part's mesh pushed out on
315            //    screen and back in depth, in its tier's ink, over the faces
316            //    but behind them: only the ring round each silhouette (over
317            //    the ground or a part further back) passes the depth test.
318            if let (Some(look), false) = (&scene.line_art, params.settings.wireframe) {
319                pass.set_pipeline(&self.hull_pipeline);
320                for (solid, gpu_solid) in &visible_solids {
321                    if solid.mesh.indices.is_empty() || translucent(solid) {
322                        continue;
323                    }
324                    let style = if look.placed.contains(&solid.name) { &self.styles.line_art_placed_outline } else { &self.styles.line_art_adding_outline };
325                    pass.set_bind_group(1, &style.bind, &[]);
326                    bind_pose(&mut pass, gpu_scene.models.as_ref(), &self.identity_model, gpu_solid.model_slot);
327                    pass.set_vertex_buffer(0, gpu_solid.vertex_buf.slice(..));
328                    pass.set_index_buffer(gpu_solid.index_buf.slice(..), wgpu::IndexFormat::Uint32);
329                    draw_visible_faces(&mut pass, solid, gpu_solid);
330                }
331            }
332
333            // 2. Occluded edge portions, dimmed (depth test inverted). Hidden
334            //    edges skip their segments (their occluded portion too).
335            if params.settings.show_edges
336                && params.settings.show_occluded_edges
337                && params.settings.hidden_edge_alpha > 0.0
338                && scene.line_art.is_none()
339            {
340                pass.set_pipeline(&self.edge_hidden_pipeline);
341                pass.set_bind_group(1, &self.styles.edge_hidden.bind, &[]);
342                for (solid, gpu_solid) in &visible_solids {
343                    let Some(edge_buf) = &gpu_solid.edge_buf else { continue };
344                    if gpu_solid.occludable_edge_instances == 0 {
345                        continue;
346                    }
347                    bind_pose(&mut pass, gpu_scene.models.as_ref(), &self.identity_model, gpu_solid.model_slot);
348                    pass.set_vertex_buffer(0, edge_buf.slice(..));
349                    if solid.visibility.any_edge_hidden() {
350                        // Single style already bound: draw only visible edges.
351                        draw_visible_edge_ranges(&mut pass, solid, gpu_solid, EdgeFilter::Real);
352                    } else {
353                        // Aux edges are not drawn occluded (`occludable_edge_instances`).
354                        pass.draw(0..6, 0..gpu_solid.occludable_edge_instances);
355                    }
356                }
357            }
358
359            // 3. Visible edges, per-edge emphasis runs; hidden edges skipped.
360            //    The whole pass is off when the Edges display toggle is.
361            if params.settings.show_edges {
362                let aux_shown = params.world_per_pixel <= AUX_EDGE_MAX_WORLD_PER_PIXEL;
363                pass.set_pipeline(&self.edge_visible_pipeline);
364                for (solid, gpu_solid) in &visible_solids {
365                    let Some(edge_buf) = &gpu_solid.edge_buf else { continue };
366                    if gpu_solid.edge_instances == 0 {
367                        continue;
368                    }
369                    bind_pose(&mut pass, gpu_scene.models.as_ref(), &self.identity_model, gpu_solid.model_slot);
370                    pass.set_vertex_buffer(0, edge_buf.slice(..));
371                    let any_edge_hidden = solid.visibility.any_edge_hidden();
372                    // The real edges are a prefix of the buffer and the AUX
373                    // ones the rest; at rest the two draw in their own styles,
374                    // and the aux ones only while the view is close enough
375                    // for them to be told apart (`AUX_EDGE_MAX_WORLD_PER_PIXEL`).
376                    let split = gpu_solid.occludable_edge_instances;
377                    // A Line art step: the real edges in the tier's ink, a picked
378                    // or hovered one in its emphasis style instead.
379                    if let Some(look) = &scene.line_art {
380                        let style = if look.placed.contains(&solid.name) { &self.styles.line_art_placed_edge } else { &self.styles.line_art_adding_edge };
381                        if !has_emphasis {
382                            pass.set_bind_group(1, &style.bind, &[]);
383                            if any_edge_hidden {
384                                draw_visible_edge_ranges(&mut pass, solid, gpu_solid, EdgeFilter::Real);
385                            } else if split > 0 {
386                                pass.draw(0..6, 0..split);
387                            }
388                            continue;
389                        }
390                        for (index, edge) in solid.edges.iter().enumerate() {
391                            let range = &gpu_solid.edges[index];
392                            if range.instance_count == 0 || edge.aux || !solid.visibility.is_edge_visible(index) {
393                                continue;
394                            }
395                            let bind = match params.emphasis.edge_state(&solid.name, &edge.name) {
396                                EmphasisState::Base => &style.bind,
397                                EmphasisState::Selected => &self.styles.edge_selected.bind,
398                                EmphasisState::Hovered => &self.styles.edge_hovered.bind,
399                            };
400                            pass.set_bind_group(1, bind, &[]);
401                            pass.draw(0..6, range.first_instance..range.first_instance + range.instance_count);
402                        }
403                        continue;
404                    }
405                    if !has_emphasis {
406                        pass.set_bind_group(1, &self.styles.edge_base.bind, &[]);
407                        if any_edge_hidden {
408                            draw_visible_edge_ranges(&mut pass, solid, gpu_solid, EdgeFilter::Real);
409                            if aux_shown {
410                                pass.set_bind_group(1, &self.styles.edge_aux.bind, &[]);
411                                draw_visible_edge_ranges(&mut pass, solid, gpu_solid, EdgeFilter::Aux);
412                            }
413                        } else {
414                            if split > 0 {
415                                pass.draw(0..6, 0..split);
416                            }
417                            if aux_shown && split < gpu_solid.edge_instances {
418                                pass.set_bind_group(1, &self.styles.edge_aux.bind, &[]);
419                                pass.draw(0..6, split..gpu_solid.edge_instances);
420                            }
421                        }
422                        continue;
423                    }
424                    // An aux edge at rest keeps its own style; hovered or
425                    // selected (its SOLID is), it emphasises like any edge.
426                    let style_for = |(state, aux): (EmphasisState, bool)| match state {
427                        EmphasisState::Base if aux => &self.styles.edge_aux.bind,
428                        EmphasisState::Base => &self.styles.edge_base.bind,
429                        EmphasisState::Selected => &self.styles.edge_selected.bind,
430                        EmphasisState::Hovered => &self.styles.edge_hovered.bind,
431                    };
432                    let mut run: Option<((EmphasisState, bool), u32, u32)> = None;
433                    let flush = |pass: &mut wgpu::RenderPass, run: &mut Option<((EmphasisState, bool), u32, u32)>| {
434                        if let Some((state, first, count)) = run.take() {
435                            if count > 0 {
436                                pass.set_bind_group(1, style_for(state), &[]);
437                                pass.draw(0..6, first..first + count);
438                            }
439                        }
440                    };
441                    for (index, edge) in solid.edges.iter().enumerate() {
442                        let range = &gpu_solid.edges[index];
443                        if range.instance_count == 0 {
444                            continue;
445                        }
446                        // Hidden edge: skip its segments and break the run.
447                        if !solid.visibility.is_edge_visible(index) || (edge.aux && !aux_shown) {
448                            flush(&mut pass, &mut run);
449                            continue;
450                        }
451                        let state = (params.emphasis.edge_state(&solid.name, &edge.name), edge.aux);
452                        match &mut run {
453                            Some((run_state, first, count))
454                                if *run_state == state
455                                    && *first + *count == range.first_instance =>
456                            {
457                                *count += range.instance_count;
458                            }
459                            _ => {
460                                flush(&mut pass, &mut run);
461                                run = Some((state, range.first_instance, range.instance_count));
462                            }
463                        }
464                    }
465                    flush(&mut pass, &mut run);
466                }
467            }
468
469            // 4. Selected/hovered face boundary outlines.
470            if has_emphasis {
471                // Bind the pipeline explicitly rather than inheriting whatever
472                // the passes above left set: pass 3 binds this same pipeline,
473                // but only when `show_edges` is on. With edges hidden the
474                // inherited pipeline was the mesh/wireframe one, whose slot 0
475                // is a Vertex-step `MeshVertex` — so the `draw(0..6, ..)` below
476                // read this Instance-step `EdgeInstance` buffer as vertices and
477                // overran it (a 4-segment boundary is 4 "vertices" at the same
478                // 24-byte stride), which wgpu reports as a fatal validation error.
479                pass.set_pipeline(&self.edge_visible_pipeline);
480                pass.set_bind_group(1, &self.styles.boundary.bind, &[]);
481                for (_, gpu_solid) in &visible_solids {
482                    let Some(boundary) = &gpu_solid.boundary else { continue };
483                    let Some(buf) = &boundary.buf else { continue };
484                    if boundary.count == 0 {
485                        continue;
486                    }
487                    pass.set_bind_group(2, &self.identity_model.bind, &[0]);
488                    pass.set_vertex_buffer(0, buf.slice(..));
489                    pass.draw(0..6, 0..boundary.count);
490                }
491            }
492
493            // 5. Vertex points; hidden vertices skip their point sprite. A
494            //    Line art step is an illustration: no vertex dots.
495            if params.settings.show_vertices && params.settings.vertex_size_px > 0.0 && scene.line_art.is_none() {
496                pass.set_pipeline(&self.point_pipeline);
497                for (solid, gpu_solid) in &visible_solids {
498                    let Some(point_buf) = &gpu_solid.point_buf else { continue };
499                    if gpu_solid.point_count == 0 {
500                        continue;
501                    }
502                    // A fully-hidden points group draws nothing — skip before touching
503                    // the per-vertex loop below. When ALL vertices are hidden that loop
504                    // would still iterate every one of them each frame; the O(N)/frame
505                    // cost is invisible on native but throttles the WebGL backend on
506                    // point-heavy models (the "can't spin after hiding points" case).
507                    if solid.visibility.all_vertices_hidden(solid.vertices.len()) {
508                        continue;
509                    }
510                    bind_pose(&mut pass, gpu_scene.models.as_ref(), &self.identity_model, gpu_solid.model_slot);
511                    pass.set_vertex_buffer(0, point_buf.slice(..));
512                    let any_vertex_hidden = solid.visibility.any_vertex_hidden();
513                    if has_emphasis || any_vertex_hidden {
514                        let tol = 1e-9_f64.max(params.world_per_pixel * 1e-3);
515                        let mut base_run: Option<(u32, u32)> = None;
516                        let mut emphasized: Vec<(EmphasisState, u32)> = Vec::new();
517                        for (index, vertex) in solid.vertices.iter().enumerate() {
518                            // Hidden vertex: skip its point and break the run.
519                            if !solid.visibility.is_vertex_visible(index) {
520                                if let Some((first, count)) = base_run.take() {
521                                    pass.set_bind_group(1, &self.styles.point_base.bind, &[]);
522                                    pass.draw(0..6, first..first + count);
523                                }
524                                continue;
525                            }
526                            let state = if has_emphasis {
527                                params.emphasis.vertex_state(&solid.name, vertex.position, tol)
528                            } else {
529                                EmphasisState::Base
530                            };
531                            if state == EmphasisState::Base {
532                                match &mut base_run {
533                                    Some((first, count)) if *first + *count == index as u32 => {
534                                        *count += 1
535                                    }
536                                    _ => {
537                                        if let Some((first, count)) = base_run.take() {
538                                            pass.set_bind_group(1, &self.styles.point_base.bind, &[]);
539                                            pass.draw(0..6, first..first + count);
540                                        }
541                                        base_run = Some((index as u32, 1));
542                                    }
543                                }
544                            } else {
545                                emphasized.push((state, index as u32));
546                            }
547                        }
548                        if let Some((first, count)) = base_run {
549                            pass.set_bind_group(1, &self.styles.point_base.bind, &[]);
550                            pass.draw(0..6, first..first + count);
551                        }
552                        for (state, index) in emphasized {
553                            let style = match state {
554                                EmphasisState::Selected => &self.styles.point_selected.bind,
555                                _ => &self.styles.point_hovered.bind,
556                            };
557                            pass.set_bind_group(1, style, &[]);
558                            pass.draw(0..6, index..index + 1);
559                        }
560                    } else {
561                        pass.set_bind_group(1, &self.styles.point_base.bind, &[]);
562                        pass.draw(0..6, 0..gpu_solid.point_count);
563                    }
564                }
565            }
566
567            // 6. World axes on top of nothing special (normal depth test).
568            if draw_axes {
569                if let Some(axis_buf) = &gpu_scene.axis_buf {
570                    pass.set_pipeline(&self.edge_visible_pipeline);
571                    pass.set_bind_group(2, &self.identity_model.bind, &[0]);
572                    pass.set_vertex_buffer(0, axis_buf.slice(..));
573                    for (index, style) in [
574                        &self.styles.axis_x,
575                        &self.styles.axis_y,
576                        &self.styles.axis_z,
577                    ]
578                    .iter()
579                    .enumerate()
580                    {
581                        pass.set_bind_group(1, &style.bind, &[]);
582                        let i = index as u32;
583                        pass.draw(0..6, i..i + 1);
584                    }
585                }
586            }
587            // Translucent solids (a motion study's transparency clip): after
588            // every opaque thing, through the blended context pipeline (depth
589            // test on, depth write off), each with its own base style, whose
590            // alpha `face_base_color` took from the display's opacity.
591            if !params.settings.wireframe && params.settings.show_faces {
592                let mut any = false;
593                for (solid, gpu_solid) in visible_solids.iter().filter(|(solid, _)| translucent(solid) && !solid.mesh.indices.is_empty()) {
594                    if !any {
595                        pass.set_pipeline(&self.context_pipeline);
596                        any = true;
597                    }
598                    bind_pose(&mut pass, gpu_scene.models.as_ref(), &self.identity_model, gpu_solid.model_slot);
599                    pass.set_vertex_buffer(0, gpu_solid.vertex_buf.slice(..));
600                    pass.set_index_buffer(gpu_solid.index_buf.slice(..), wgpu::IndexFormat::Uint32);
601                    pass.set_bind_group(1, &gpu_solid.base_style.bind, &[]);
602                    if !solid.visibility.any_face_hidden() {
603                        pass.draw_indexed(0..solid.mesh.indices.len() as u32, 0, 0..1);
604                        continue;
605                    }
606                    for (index, face) in gpu_solid.faces.iter().enumerate() {
607                        if solid.visibility.is_face_visible(index) {
608                            pass.draw_indexed(face.first_index..face.first_index + face.index_count, 0, 0..1);
609                        }
610                    }
611                }
612            }
613            if let Some((gpu_context, context_scene)) = context {
614                // Blend without depth writes: surroundings must not hide the
615                // editable geometry. Keep the work part's depth buffer intact.
616                self.context_style.write(&self.queue, context_scene.tint.unwrap_or(super::WORK_PART_CONTEXT_RGBA), [0.0; 4]);
617                pass.set_pipeline(&self.context_pipeline);
618                pass.set_bind_group(1, &self.context_style.bind, &[]);
619                for solid in context_scene.solids().iter().filter(|solid| solid.visible) {
620                    let Some(gpu) = gpu_context.solids.get(&solid.name) else { continue };
621                    bind_pose(&mut pass, gpu_context.models.as_ref(), &self.identity_model, gpu.model_slot);
622                    pass.set_vertex_buffer(0, gpu.vertex_buf.slice(..));
623                    pass.set_index_buffer(gpu.index_buf.slice(..), wgpu::IndexFormat::Uint32);
624                    if !solid.visibility.any_face_hidden() {
625                        pass.draw_indexed(0..solid.mesh.indices.len() as u32, 0, 0..1);
626                        continue;
627                    }
628                    for (index, face) in gpu.faces.iter().enumerate() {
629                        if solid.visibility.is_face_visible(index) {
630                            pass.draw_indexed(face.first_index..face.first_index + face.index_count, 0, 0..1);
631                        }
632                    }
633                }
634                // The context face picked or hovered, over its translucent self;
635                // or the context edge a sketch would copy, as a lit edge.
636                for (solid_name, face_name, selected) in &context_scene.highlight {
637                    let Some(solid) = context_scene.solid(solid_name) else { continue };
638                    let Some(gpu) = gpu_context.solids.get(solid_name) else { continue };
639                    let Some(index) = solid.faces.iter().position(|face| &face.name == face_name) else {
640                        let edge = solid.edges.iter().position(|edge| &edge.name == face_name);
641                        let (Some(index), Some(edge_buf)) = (edge, &gpu.edge_buf) else { continue };
642                        let Some(range) = gpu.edges.get(index) else { continue };
643                        pass.set_pipeline(&self.edge_visible_pipeline);
644                        pass.set_bind_group(1, if *selected { &self.styles.edge_selected.bind } else { &self.styles.edge_hovered.bind }, &[]);
645                        bind_pose(&mut pass, gpu_context.models.as_ref(), &self.identity_model, gpu.model_slot);
646                        pass.set_vertex_buffer(0, edge_buf.slice(..));
647                        pass.draw(0..6, range.first_instance..range.first_instance + range.instance_count);
648                        pass.set_pipeline(&self.context_pipeline);
649                        continue;
650                    };
651                    let Some(range) = gpu.faces.get(index) else { continue };
652                    pass.set_bind_group(1, if *selected { &self.context_selected.bind } else { &self.context_hovered.bind }, &[]);
653                    bind_pose(&mut pass, gpu_context.models.as_ref(), &self.identity_model, gpu.model_slot);
654                    pass.set_vertex_buffer(0, gpu.vertex_buf.slice(..));
655                    pass.set_index_buffer(gpu.index_buf.slice(..), wgpu::IndexFormat::Uint32);
656                    pass.draw_indexed(range.first_index..range.first_index + range.index_count, 0, 0..1);
657                }
658            }
659        }
660
661        // --- Overlay-widget passes: the brep-gizmos overlay drawn
662        //     over the solids in a depth-cleared pass so widgets read on top.
663        //     The main overlay uses the scene camera + full viewport; the
664        //     ViewCube uses its own mini-camera + corner viewport.
665        if let Some(overlay) = params.overlay {
666            let make_tris = |ov: &brep_gizmos::Overlay| -> Option<wgpu::Buffer> {
667                let verts = overlay_tri_verts(ov);
668                (!verts.is_empty()).then(|| {
669                    self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
670                        label: Some("overlay tris"),
671                        contents: bytemuck::cast_slice(&verts),
672                        usage: wgpu::BufferUsages::VERTEX,
673                    })
674                })
675            };
676            let make_lines = |ov: &brep_gizmos::Overlay| -> Option<wgpu::Buffer> {
677                let insts = overlay_line_insts(ov);
678                (!insts.is_empty()).then(|| {
679                    self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
680                        label: Some("overlay lines"),
681                        contents: bytemuck::cast_slice(&insts),
682                        usage: wgpu::BufferUsages::VERTEX,
683                    })
684                })
685            };
686
687            let main_tri_count = overlay.main.tris.len() as u32;
688            let main_line_count = (overlay.main.lines.len() / 2) as u32;
689            let main_tri_buf = make_tris(&overlay.main);
690            let main_line_buf = make_lines(&overlay.main);
691            if main_tri_buf.is_some() || main_line_buf.is_some() {
692                let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
693                    label: Some("overlay-main"),
694                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
695                        view: &targets.msaa_view,
696                        depth_slice: None,
697                        resolve_target: Some(resolve_view),
698                        ops: wgpu::Operations {
699                            load: wgpu::LoadOp::Load,
700                            store: wgpu::StoreOp::Store,
701                        },
702                    })],
703                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
704                        view: &targets.depth_view,
705                        depth_ops: Some(wgpu::Operations {
706                            load: wgpu::LoadOp::Clear(1.0),
707                            store: wgpu::StoreOp::Discard,
708                        }),
709                        stencil_ops: None,
710                    }),
711                    timestamp_writes: None,
712                    occlusion_query_set: None,
713                    multiview_mask: None,
714                });
715                pass.set_bind_group(0, &self.globals_bind, &[]);
716                pass.set_bind_group(1, &self.styles.overlay_line.bind, &[]);
717                if let Some(buf) = &main_tri_buf {
718                    pass.set_vertex_buffer(0, buf.slice(..));
719                    // The datum/construction PLANE tris come FIRST; draw them with
720                    // depth-write OFF so a translucent plane never occludes the
721                    // gizmo/dimension tris that follow. The rest keep depth-write
722                    // so gizmos self-occlude correctly.
723                    let plane_verts = (overlay.plane_tri_verts as u32).min(main_tri_count);
724                    if plane_verts > 0 {
725                        pass.set_pipeline(&self.overlay_tri_nodepth_pipeline);
726                        pass.draw(0..plane_verts, 0..1);
727                    }
728                    if plane_verts < main_tri_count {
729                        pass.set_pipeline(&self.overlay_tri_pipeline);
730                        pass.draw(plane_verts..main_tri_count, 0..1);
731                    }
732                }
733                if let Some(buf) = &main_line_buf {
734                    pass.set_pipeline(&self.overlay_line_pipeline);
735                    pass.set_vertex_buffer(0, buf.slice(..));
736                    pass.draw(0..6, 0..main_line_count);
737                }
738            }
739
740            if let Some(vc) = &overlay.viewcube {
741                let dpr = params.dpr.max(1e-3);
742                let mut x = (vc.rect_css[0] * dpr).max(0.0);
743                let mut y = (vc.rect_css[1] * dpr).max(0.0);
744                let mut w = (vc.rect_css[2] * dpr).max(1.0);
745                let mut h = (vc.rect_css[3] * dpr).max(1.0);
746                // Clamp the corner viewport to the framebuffer.
747                w = w.min(width as f32 - x).max(1.0);
748                h = h.min(height as f32 - y).max(1.0);
749                x = x.min(width as f32 - w).max(0.0);
750                y = y.min(height as f32 - h).max(0.0);
751
752                let vc_globals = Globals {
753                    view_proj: vc.view_proj,
754                    viewport: [w, h, dpr, 0.0],
755                    forward: [vc.forward[0], vc.forward[1], vc.forward[2], 0.0],
756                };
757                self.queue
758                    .write_buffer(&self.vc_globals_buf, 0, bytemuck::bytes_of(&vc_globals));
759
760                let vc_tri_count = vc.overlay.tris.len() as u32;
761                let vc_line_count = (vc.overlay.lines.len() / 2) as u32;
762                let vc_tri_buf = make_tris(&vc.overlay);
763                let vc_line_buf = make_lines(&vc.overlay);
764                if vc_tri_buf.is_some() || vc_line_buf.is_some() {
765                    let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
766                        label: Some("overlay-viewcube"),
767                        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
768                            view: &targets.msaa_view,
769                            depth_slice: None,
770                            resolve_target: Some(resolve_view),
771                            ops: wgpu::Operations {
772                                load: wgpu::LoadOp::Load,
773                                store: wgpu::StoreOp::Store,
774                            },
775                        })],
776                        depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
777                            view: &targets.depth_view,
778                            depth_ops: Some(wgpu::Operations {
779                                load: wgpu::LoadOp::Clear(1.0),
780                                store: wgpu::StoreOp::Discard,
781                            }),
782                            stencil_ops: None,
783                        }),
784                        timestamp_writes: None,
785                        occlusion_query_set: None,
786                        multiview_mask: None,
787                    });
788                    pass.set_viewport(x, y, w, h, 0.0, 1.0);
789                    pass.set_scissor_rect(x as u32, y as u32, w as u32, h as u32);
790                    pass.set_bind_group(0, &self.vc_globals_bind, &[]);
791                    pass.set_bind_group(1, &self.styles.overlay_line.bind, &[]);
792                    if let Some(buf) = &vc_tri_buf {
793                        pass.set_pipeline(&self.overlay_tri_pipeline);
794                        pass.set_vertex_buffer(0, buf.slice(..));
795                        pass.draw(0..vc_tri_count, 0..1);
796                    }
797                    if let Some(buf) = &vc_line_buf {
798                        pass.set_pipeline(&self.overlay_line_pipeline);
799                        pass.set_vertex_buffer(0, buf.slice(..));
800                        pass.draw(0..6, 0..vc_line_count);
801                    }
802                }
803            }
804        }
805
806        self.queue.submit([encoder.finish()]);
807    }
808
809    /// Headless capture (R32/R34): render the scene and return PNG bytes
810    /// (8-bit RGB, no ancillary chunks — deterministic, R33).
811    #[cfg(not(target_arch = "wasm32"))]
812    pub fn render_to_png(
813        &mut self,
814        scene: &RenderScene,
815        camera: &Camera,
816        width: u32,
817        height: u32,
818    ) -> Result<Vec<u8>, String> {
819        let settings = RenderSettings::artifact();
820        let mut gpu_scene = self.upload_scene_with(scene, &settings);
821        self.render_gpu_scene_to_png(&mut gpu_scene, scene, camera, width, height)
822    }
823
824    /// [`Self::render_to_png`] from buffers the caller already holds (synced
825    /// with [`RenderSettings::artifact`]): what the viewport's retained GPU
826    /// scene would draw.
827    #[cfg(not(target_arch = "wasm32"))]
828    pub(crate) fn render_gpu_scene_to_png(
829        &mut self,
830        gpu_scene: &mut GpuScene,
831        scene: &RenderScene,
832        camera: &Camera,
833        width: u32,
834        height: u32,
835    ) -> Result<Vec<u8>, String> {
836        let settings = RenderSettings::artifact();
837        let rgb = self.render_gpu_scene_to_rgb(gpu_scene, scene, camera, width, height, &settings)?;
838        encode_png(&rgb, width, height)
839    }
840
841    /// Draw `scene` through `gpu_scene` (synced or uploaded by the caller) at
842    /// `width`×`height` under `settings` and read the frame back as packed
843    /// RGB, top row first: what a motion study's video export encodes frame
844    /// by frame with the viewport's own render core and the user's settings.
845    #[cfg(not(target_arch = "wasm32"))]
846    pub fn render_gpu_scene_to_rgb(
847        &mut self,
848        gpu_scene: &mut GpuScene,
849        scene: &RenderScene,
850        camera: &Camera,
851        width: u32,
852        height: u32,
853        settings: &RenderSettings,
854    ) -> Result<Vec<u8>, String> {
855        let emphasis = Emphasis::default();
856        let params = FrameParams {
857            camera,
858            width,
859            height,
860            dpr: 1.0,
861            settings,
862            emphasis: &emphasis,
863            world_per_pixel: 0.0,
864            overlay: None,
865        };
866        let resolve = self.device.create_texture(&wgpu::TextureDescriptor {
867            label: Some("resolve"),
868            size: wgpu::Extent3d {
869                width,
870                height,
871                depth_or_array_layers: 1,
872            },
873            mip_level_count: 1,
874            sample_count: 1,
875            dimension: wgpu::TextureDimension::D2,
876            format: self.format,
877            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
878            view_formats: &[],
879        });
880        let resolve_view = resolve.create_view(&Default::default());
881        self.render_to_view(gpu_scene, scene, &params, &resolve_view);
882
883        // Readback: rows padded to 256 bytes per wgpu's copy alignment.
884        let bytes_per_row = (width * 4).div_ceil(256) * 256;
885        let readback = self.device.create_buffer(&wgpu::BufferDescriptor {
886            label: Some("readback"),
887            size: bytes_per_row as u64 * height as u64,
888            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
889            mapped_at_creation: false,
890        });
891        let mut encoder = self
892            .device
893            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
894                label: Some("readback"),
895            });
896        encoder.copy_texture_to_buffer(
897            wgpu::TexelCopyTextureInfo {
898                texture: &resolve,
899                mip_level: 0,
900                origin: wgpu::Origin3d::ZERO,
901                aspect: wgpu::TextureAspect::All,
902            },
903            wgpu::TexelCopyBufferInfo {
904                buffer: &readback,
905                layout: wgpu::TexelCopyBufferLayout {
906                    offset: 0,
907                    bytes_per_row: Some(bytes_per_row),
908                    rows_per_image: None,
909                },
910            },
911            wgpu::Extent3d {
912                width,
913                height,
914                depth_or_array_layers: 1,
915            },
916        );
917        self.queue.submit([encoder.finish()]);
918
919        let slice = readback.slice(..);
920        let (sender, receiver) = std::sync::mpsc::channel();
921        slice.map_async(wgpu::MapMode::Read, move |result| {
922            let _ = sender.send(result);
923        });
924        self.device
925            .poll(wgpu::PollType::wait_indefinitely())
926            .map_err(|error| format!("wgpu poll: {error:?}"))?;
927        receiver
928            .recv()
929            .map_err(|_| "readback callback dropped".to_string())?
930            .map_err(|error| format!("readback map failed: {error:?}"))?;
931
932        let data = slice.get_mapped_range();
933        let mut rgb = Vec::with_capacity((width * height * 3) as usize);
934        for row in 0..height {
935            let start = (row * bytes_per_row) as usize;
936            for col in 0..width as usize {
937                let px = start + col * 4;
938                rgb.extend_from_slice(&data[px..px + 3]);
939            }
940        }
941        drop(data);
942        readback.unmap();
943        Ok(rgb)
944    }
945}
946
947/// Issue draw calls for a solid's VISIBLE edge instances only, coalescing
948/// contiguous edge ranges into as few `draw`s as possible. Used by the
949/// single-style edge passes (occluded/hidden, and the no-emphasis visible pass)
950/// when the solid has any hidden edge — the caller has already bound the style.
951/// The coarsest view, in world millimetres per CSS pixel, that still draws AUX
952/// edges — a board's copper outline. Past it a 0.2 mm track is under two
953/// pixels, so its outline would cover it instead of edging it and a dense track
954/// field reads as one slab; and a board seen whole at that scale is exactly
955/// when the most of its thousands of outline segments are on screen at once.
956/// Zooming in brings them back.
957const AUX_EDGE_MAX_WORLD_PER_PIXEL: f64 = 0.1;
958
959/// Which of a solid's edges a [`draw_visible_edge_ranges`] call draws.
960#[derive(Clone, Copy, PartialEq, Eq)]
961enum EdgeFilter {
962    /// The solid's own edges, not its display-only (aux) ones.
963    Real,
964    /// Only the display-only ones.
965    Aux,
966}
967
968/// Draw a solid's triangles with whatever style is bound, its hidden faces
969/// left out (contiguous visible faces drawn as one range).
970fn draw_visible_faces(pass: &mut wgpu::RenderPass<'_>, solid: &SolidDisplay, gpu_solid: &GpuSolid) {
971    if !solid.visibility.any_face_hidden() {
972        pass.draw_indexed(0..solid.mesh.indices.len() as u32, 0, 0..1);
973        return;
974    }
975    let mut run: Option<(u32, u32)> = None; // first_index, count
976    for index in 0..solid.faces.len() {
977        let range = &gpu_solid.faces[index];
978        if range.index_count == 0 {
979            continue;
980        }
981        if !solid.visibility.is_face_visible(index) {
982            if let Some((first, count)) = run.take() {
983                pass.draw_indexed(first..first + count, 0, 0..1);
984            }
985            continue;
986        }
987        match &mut run {
988            Some((first, count)) if *first + *count == range.first_index => *count += range.index_count,
989            _ => {
990                if let Some((first, count)) = run.take() {
991                    pass.draw_indexed(first..first + count, 0, 0..1);
992                }
993                run = Some((range.first_index, range.index_count));
994            }
995        }
996    }
997    if let Some((first, count)) = run {
998        pass.draw_indexed(first..first + count, 0, 0..1);
999    }
1000}
1001
1002fn draw_visible_edge_ranges(
1003    pass: &mut wgpu::RenderPass<'_>,
1004    solid: &SolidDisplay,
1005    gpu_solid: &GpuSolid,
1006    filter: EdgeFilter,
1007) {
1008    let mut run: Option<(u32, u32)> = None; // first_instance, count
1009    for (index, edge) in solid.edges.iter().enumerate() {
1010        let range = &gpu_solid.edges[index];
1011        if range.instance_count == 0 {
1012            continue;
1013        }
1014        if !solid.visibility.is_edge_visible(index) || edge.aux != (filter == EdgeFilter::Aux) {
1015            if let Some((first, count)) = run.take() {
1016                pass.draw(0..6, first..first + count);
1017            }
1018            continue;
1019        }
1020        match &mut run {
1021            Some((first, count)) if *first + *count == range.first_instance => {
1022                *count += range.instance_count;
1023            }
1024            _ => {
1025                if let Some((first, count)) = run.take() {
1026                    pass.draw(0..6, first..first + count);
1027                }
1028                run = Some((range.first_instance, range.instance_count));
1029            }
1030        }
1031    }
1032    if let Some((first, count)) = run {
1033        pass.draw(0..6, first..first + count);
1034    }
1035}
1036
1037/// Convert a gizmo `Overlay`'s triangles into GPU vertices (per-vertex color).
1038fn overlay_tri_verts(ov: &brep_gizmos::Overlay) -> Vec<OverlayTriVertex> {
1039    ov.tris
1040        .iter()
1041        .map(|v| OverlayTriVertex {
1042            position: v.pos,
1043            normal: v.normal,
1044            color: v.color,
1045        })
1046        .collect()
1047}
1048
1049/// Convert a gizmo `Overlay`'s line segments (vertex pairs) into GPU instances
1050/// (per-instance color; both endpoints of a gizmo segment share a color).
1051fn overlay_line_insts(ov: &brep_gizmos::Overlay) -> Vec<OverlayLineInstance> {
1052    ov.lines
1053        .chunks_exact(2)
1054        .map(|pair| OverlayLineInstance {
1055            p0: pair[0].pos,
1056            p1: pair[1].pos,
1057            color: pair[0].color,
1058        })
1059        .collect()
1060}
1061
1062/// Bind a solid's pose: its slot of the scene's pose buffer (identity when
1063/// the scene holds none yet).
1064fn bind_pose(pass: &mut wgpu::RenderPass<'_>, models: Option<&ModelArena>, identity: &IdentityModel, slot: u32) {
1065    match models {
1066        Some(models) => pass.set_bind_group(2, &models.bind, &[ModelArena::offset(slot)]),
1067        None => pass.set_bind_group(2, &identity.bind, &[0]),
1068    }
1069}