Skip to main content

brep_render/render/
scene_sync.rs

1use super::*;
2
3/// An AUX edge's width at rest, in CSS px ([`GlobalStyles::edge_aux`]).
4const AUX_EDGE_WIDTH_PX: f32 = 1.0;
5
6impl RenderCore {
7    /// The base face color of one solid under the active settings, in LINEAR
8    /// space (converted with f64 precision so artifact bytes stay stable).
9    fn face_base_color(solid: &SolidDisplay, settings: &RenderSettings) -> Rgba {
10        if let Some(rgb) = solid.color_override {
11            return Self::opaque_linear(rgb);
12        }
13        match settings.face_color_mode {
14            FaceColorMode::HashedBySolid => {
15                let srgb = solid_color_srgb(&solid.name);
16                [
17                    srgb_to_linear(srgb[0]) as f32,
18                    srgb_to_linear(srgb[1]) as f32,
19                    srgb_to_linear(srgb[2]) as f32,
20                    1.0,
21                ]
22            }
23            FaceColorMode::Uniform => {
24                let c = settings.face_color;
25                [
26                    srgb_to_linear(c[0] as f64) as f32,
27                    srgb_to_linear(c[1] as f64) as f32,
28                    srgb_to_linear(c[2] as f64) as f32,
29                    c[3],
30                ]
31            }
32        }
33    }
34
35    /// One sRGB model colour as an opaque LINEAR `Rgba` — the conversion the
36    /// solid's base style and the per-face palette must share.
37    fn opaque_linear(rgb: [f32; 3]) -> Rgba {
38        [
39            srgb_to_linear(rgb[0] as f64) as f32,
40            srgb_to_linear(rgb[1] as f64) as f32,
41            srgb_to_linear(rgb[2] as f64) as f32,
42            1.0,
43        ]
44    }
45
46    /// The shading params every face style carries (flat-vs-smooth in `.x`).
47    fn style_params(settings: &RenderSettings) -> [f32; 4] {
48        [if settings.flat_shading { 1.0 } else { 0.0 }, 0.0, 0.0, 0.0]
49    }
50
51    /// Build this solid's PER-FACE style palette, de-duplicated by colour, and
52    /// the per-face index into it.
53    ///
54    /// Faces sharing a colour share one uniform buffer, so an imported body
55    /// painted in three colours costs three buffers however many faces it has —
56    /// and the draw loop can still coalesce a contiguous run of same-coloured
57    /// faces into one call. A solid with no per-face colours returns an EMPTY
58    /// palette, which is what re-arms the whole-mesh fast path.
59    fn face_style_palette(
60        &self,
61        solid: &SolidDisplay,
62        settings: &RenderSettings,
63    ) -> (Vec<StyleBuf>, Vec<Option<u32>>) {
64        let mut styles: Vec<StyleBuf> = Vec::new();
65        let mut colors: Vec<[f32; 3]> = Vec::new();
66        let mut per_face: Vec<Option<u32>> = Vec::with_capacity(solid.faces.len());
67        let params = Self::style_params(settings);
68        for face in &solid.faces {
69            let Some(rgb) = face.color_override else {
70                per_face.push(None);
71                continue;
72            };
73            let slot = match colors.iter().position(|c| *c == rgb) {
74                Some(slot) => slot,
75                None => {
76                    let style = StyleBuf::new(
77                        &self.device,
78                        &self.style_layout,
79                        solid.name.as_str(),
80                    );
81                    style.write(&self.queue, Self::opaque_linear(rgb), params);
82                    styles.push(style);
83                    colors.push(rgb);
84                    colors.len() - 1
85                }
86            };
87            per_face.push(Some(slot as u32));
88        }
89        (styles, per_face)
90    }
91
92    fn upload_solid(&self, solid: &SolidDisplay, settings: &RenderSettings) -> GpuSolid {
93        let vertices: Vec<MeshVertex> = solid
94            .mesh
95            .positions
96            .iter()
97            .zip(&solid.mesh.normals)
98            .map(|(&position, &normal)| MeshVertex { position, normal })
99            .collect();
100        let vertex_buf = self
101            .device
102            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
103                label: Some(solid.name.as_str()),
104                contents: bytemuck::cast_slice(&vertices),
105                usage: wgpu::BufferUsages::VERTEX,
106            });
107        let index_buf = self
108            .device
109            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
110                label: Some(solid.name.as_str()),
111                contents: bytemuck::cast_slice(&solid.mesh.indices),
112                usage: wgpu::BufferUsages::INDEX,
113            });
114        // Wireframe line-list, built PER FACE and de-duplicated inside each face.
115        //
116        // The obvious expansion — every triangle (a,b,c) → (a,b),(b,c),(c,a) —
117        // emits each of a face's INTERIOR edges twice, once from each of the two
118        // triangles that share it. That is two-thirds of the segments on a typical
119        // tessellation: twice the index data to upload and hold, and twice the
120        // vertex work per frame to draw two collinear lines on top of each other.
121        // The wire pipeline does not blend (`color_target(None)` in `pipelines.rs`),
122        // so the second draw of a segment lands on the same pixels in the same
123        // colour at the same depth — it was never visible.
124        //
125        // De-duplicating WITHIN a face, rather than across the whole mesh, is what
126        // keeps each face's segments CONTIGUOUS, which is what lets the hidden-face
127        // path below address them by range the way the shaded path addresses
128        // triangle ranges. The seam between two faces stays doubled (once per
129        // owning face) — a small fraction, and the price of a face still being able
130        // to hide its own wireframe.
131        let tri_total = solid.mesh.indices.len() / 3;
132        let mut wire_indices: Vec<u32> = Vec::with_capacity(solid.mesh.indices.len());
133        let mut wire_ranges: Vec<(u32, u32)> = Vec::with_capacity(solid.faces.len());
134        let mut covered = vec![false; tri_total];
135        let mut seen: HashSet<(u32, u32), BuildEdgeHasher> = HashSet::default();
136        let mut emit_triangle = |wire: &mut Vec<u32>,
137                                 seen: &mut HashSet<(u32, u32), BuildEdgeHasher>,
138                                 tri: usize| {
139            let idx = [
140                solid.mesh.indices[tri * 3],
141                solid.mesh.indices[tri * 3 + 1],
142                solid.mesh.indices[tri * 3 + 2],
143            ];
144            for k in 0..3 {
145                let (a, b) = (idx[k], idx[(k + 1) % 3]);
146                let key = if a < b { (a, b) } else { (b, a) };
147                if seen.insert(key) {
148                    wire.push(a);
149                    wire.push(b);
150                }
151            }
152        };
153        for face in &solid.faces {
154            let first = wire_indices.len() as u32;
155            seen.clear();
156            let start = (face.tri_start as usize).min(tri_total);
157            let end = (start + face.tri_count as usize).min(tri_total);
158            for tri in start..end {
159                covered[tri] = true;
160                emit_triangle(&mut wire_indices, &mut seen, tri);
161            }
162            wire_ranges.push((first, wire_indices.len() as u32 - first));
163        }
164        // Any triangle no face claims still belongs in the whole-mesh draw — the
165        // fast path draws the buffer end to end. It is addressed by no face range,
166        // exactly as before: the per-face path could never reach it either.
167        seen.clear();
168        for tri in 0..tri_total {
169            if !covered[tri] {
170                emit_triangle(&mut wire_indices, &mut seen, tri);
171            }
172        }
173        let wire_index_count = wire_indices.len() as u32;
174        let wire_index_buf = self
175            .device
176            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
177                label: Some(solid.name.as_str()),
178                contents: bytemuck::cast_slice(&wire_indices),
179                usage: wgpu::BufferUsages::INDEX,
180            });
181        let (face_styles, face_style_index) = self.face_style_palette(solid, settings);
182        let faces = solid
183            .faces
184            .iter()
185            .zip(&face_style_index)
186            .zip(&wire_ranges)
187            .map(|((face, style), &(wire_first, wire_count))| FaceRange {
188                first_index: face.tri_start * 3,
189                index_count: face.tri_count * 3,
190                wire_first,
191                wire_count,
192                style: *style,
193            })
194            .collect();
195
196        let mut segments: Vec<EdgeInstance> = Vec::new();
197        let mut edges = Vec::with_capacity(solid.edges.len());
198        for edge in &solid.edges {
199            let first_instance = segments.len() as u32;
200            for pair in edge.polyline.windows(2) {
201                segments.push(EdgeInstance {
202                    p0: pair[0],
203                    p1: pair[1],
204                });
205            }
206            edges.push(EdgeRange {
207                first_instance,
208                instance_count: segments.len() as u32 - first_instance,
209            });
210        }
211        let occludable_edge_instances = solid
212            .edges
213            .iter()
214            .zip(&edges)
215            .find(|(edge, _)| edge.aux)
216            .map_or(segments.len() as u32, |(_, range): (_, &EdgeRange)| range.first_instance);
217        let edge_buf = (!segments.is_empty()).then(|| {
218            self.device
219                .create_buffer_init(&wgpu::util::BufferInitDescriptor {
220                    label: Some(solid.name.as_str()),
221                    contents: bytemuck::cast_slice(&segments),
222                    usage: wgpu::BufferUsages::VERTEX,
223                })
224        });
225
226        let points: Vec<PointInstance> = solid
227            .vertices
228            .iter()
229            .map(|v| PointInstance {
230                center: [
231                    v.position[0] as f32,
232                    v.position[1] as f32,
233                    v.position[2] as f32,
234                ],
235            })
236            .collect();
237        let point_buf = (!points.is_empty()).then(|| {
238            self.device
239                .create_buffer_init(&wgpu::util::BufferInitDescriptor {
240                    label: Some(solid.name.as_str()),
241                    contents: bytemuck::cast_slice(&points),
242                    usage: wgpu::BufferUsages::VERTEX,
243                })
244        });
245
246        let base_style = StyleBuf::new(&self.device, &self.style_layout, solid.name.as_str());
247        base_style.write(
248            &self.queue,
249            Self::face_base_color(solid, settings),
250            Self::style_params(settings),
251        );
252
253        GpuSolid {
254            revision: solid.revision,
255            face_styles,
256            vertex_buf,
257            index_buf,
258            wire_index_buf,
259            wire_index_count,
260            faces,
261            edge_buf,
262            edges,
263            edge_instances: segments.len() as u32,
264            occludable_edge_instances,
265            point_buf,
266            point_count: points.len() as u32,
267            base_style,
268            boundary: None,
269        }
270    }
271
272    /// Bring the GPU scene in line with the display scene: new/changed solids
273    /// upload, unchanged solids keep their buffers (R10), removed solids drop
274    /// theirs. `settings_generation` invalidates per-solid style buffers only.
275    pub fn sync_scene(
276        &self,
277        gpu: &mut GpuScene,
278        scene: &RenderScene,
279        settings: &RenderSettings,
280        settings_generation: u64,
281    ) {
282        gpu.order.clear();
283        let mut seen: Vec<&str> = Vec::with_capacity(scene.solids().len());
284        for solid in scene.solids() {
285            gpu.order.push(solid.name.clone());
286            seen.push(solid.name.as_str());
287            let refresh_style = gpu.settings_generation != settings_generation;
288            match gpu.solids.get_mut(&solid.name) {
289                Some(existing) if existing.revision == solid.revision => {
290                    if refresh_style {
291                        let params = Self::style_params(settings);
292                        existing.base_style.write(
293                            &self.queue,
294                            Self::face_base_color(solid, settings),
295                            params,
296                        );
297                        // The per-face palette carries the same shading params,
298                        // so it restyles with the base or flat shading would
299                        // stop applying to individually coloured faces.
300                        let mut seen: Vec<[f32; 3]> = Vec::new();
301                        for face in &solid.faces {
302                            let Some(rgb) = face.color_override else { continue };
303                            if seen.contains(&rgb) {
304                                continue;
305                            }
306                            if let Some(style) = existing.face_styles.get(seen.len()) {
307                                style.write(&self.queue, Self::opaque_linear(rgb), params);
308                            }
309                            seen.push(rgb);
310                        }
311                    }
312                }
313                _ => {
314                    let uploaded = self.upload_solid(solid, settings);
315                    gpu.solids.insert(solid.name.clone(), uploaded);
316                    gpu.uploads += 1;
317                }
318            }
319        }
320        gpu.solids.retain(|name, _| seen.contains(&name.as_str()));
321        gpu.settings_generation = settings_generation;
322    }
323
324    /// Upload a scene from scratch (artifact/one-shot path).
325    pub fn upload_scene(&self, scene: &RenderScene) -> GpuScene {
326        self.upload_scene_with(scene, &RenderSettings::artifact())
327    }
328
329    pub fn upload_scene_with(&self, scene: &RenderScene, settings: &RenderSettings) -> GpuScene {
330        let mut gpu = GpuScene::default();
331        self.sync_scene(&mut gpu, scene, settings, 0);
332        gpu
333    }
334
335    pub(super) fn ensure_targets(&mut self, width: u32, height: u32) {
336        if let Some(targets) = &self.targets {
337            if targets.width == width && targets.height == height {
338                return;
339            }
340        }
341        let size = wgpu::Extent3d {
342            width,
343            height,
344            depth_or_array_layers: 1,
345        };
346        let msaa = self.device.create_texture(&wgpu::TextureDescriptor {
347            label: Some("msaa color"),
348            size,
349            mip_level_count: 1,
350            sample_count: SAMPLES,
351            dimension: wgpu::TextureDimension::D2,
352            format: self.format,
353            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
354            view_formats: &[],
355        });
356        let depth = self.device.create_texture(&wgpu::TextureDescriptor {
357            label: Some("depth"),
358            size,
359            mip_level_count: 1,
360            sample_count: SAMPLES,
361            dimension: wgpu::TextureDimension::D2,
362            format: DEPTH_FORMAT,
363            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
364            view_formats: &[],
365        });
366        self.targets = Some(CachedTargets {
367            width,
368            height,
369            msaa_view: msaa.create_view(&Default::default()),
370            depth_view: depth.create_view(&Default::default()),
371        });
372    }
373
374    pub(super) fn write_global_styles(&self, settings: &RenderSettings) {
375        let flat = if settings.flat_shading { 1.0 } else { 0.0 };
376        let face = |c: Rgba| -> Rgba {
377            [
378                srgb_to_linear(c[0] as f64) as f32,
379                srgb_to_linear(c[1] as f64) as f32,
380                srgb_to_linear(c[2] as f64) as f32,
381                c[3],
382            ]
383        };
384        let s = &self.styles;
385        let q = &self.queue;
386        s.face_selected
387            .write(q, face(settings.face_selected_color), [flat, 0.0, 0.0, 0.0]);
388        s.face_hovered
389            .write(q, face(settings.hover_color), [flat, 0.0, 0.0, 0.0]);
390        let edge_params = [settings.edge_width_px, EDGE_NUDGE, 0.0, 0.0];
391        s.edge_base.write(q, settings.edge_color, edge_params);
392        s.edge_selected.write(q, settings.edge_selected_color, edge_params);
393        s.edge_hovered.write(q, settings.hover_color, edge_params);
394        let mut hidden = settings.edge_color;
395        hidden[3] = settings.hidden_edge_alpha;
396        s.edge_hidden.write(q, hidden, edge_params);
397        // An AUX edge at rest is the theme's edge at hairline WIDTH: a board's
398        // copper outline is thousands of edges a fraction of a millimetre
399        // apart, and at the full width the outlines of a track field crowd
400        // the gaps between its tracks. (Too coarse a view does not draw them
401        // at all — `frame::AUX_EDGE_MAX_WORLD_PER_PIXEL`.)
402        s.edge_aux.write(q, settings.edge_color, [AUX_EDGE_WIDTH_PX, EDGE_NUDGE, 0.0, 0.0]);
403        // Selected-face boundary: selected-edge color, slightly wider, extra
404        // nudge so it reads as an outline on top of the fill.
405        s.boundary.write(
406            q,
407            settings.edge_selected_color,
408            [settings.edge_width_px + 1.0, EDGE_NUDGE * 1.5, 0.0, 0.0],
409        );
410        let point_params = [settings.vertex_size_px, 0.0, 0.0, 0.0];
411        s.point_base.write(q, settings.vertex_color, point_params);
412        s.point_selected
413            .write(q, settings.vertex_selected_color, [settings.vertex_size_px + 1.0, 0.0, 0.0, 0.0]);
414        s.point_hovered
415            .write(q, settings.hover_color, [settings.vertex_size_px + 1.0, 0.0, 0.0, 0.0]);
416        let axis_params = [2.0, EDGE_NUDGE, 0.0, 0.0];
417        s.axis_x.write(q, [0.91, 0.30, 0.32, 1.0], axis_params);
418        s.axis_y.write(q, [0.27, 0.80, 0.42, 1.0], axis_params);
419        s.axis_z.write(q, [0.23, 0.51, 0.96, 1.0], axis_params);
420        // Overlay-widget line width (color is per-instance from the gizmo).
421        s.overlay_line.write(q, [1.0, 1.0, 1.0, 1.0], [2.5, 0.0, 0.0, 0.0]);
422    }
423
424    /// Rebuild the selected/hovered-face boundary outline buffer for one solid
425    /// when the emphasis or the solid changed. The boundary of a face's
426    /// triangle range = mesh edges used an odd number of times inside it.
427    pub(super) fn sync_boundary(&self, gpu_solid: &mut GpuSolid, solid: &SolidDisplay, emphasis: &Emphasis) {
428        if let Some(boundary) = &gpu_solid.boundary {
429            if boundary.emphasis_generation == emphasis.generation
430                && boundary.revision == solid.revision
431            {
432                return;
433            }
434        }
435        let mut segments: Vec<EdgeInstance> = Vec::new();
436        for face in &solid.faces {
437            if face.tri_count == 0 {
438                continue;
439            }
440            let state = emphasis.face_state(&solid.name, &face.name);
441            if state == EmphasisState::Base {
442                continue;
443            }
444            let mut edge_use: HashMap<(u32, u32), u32> = HashMap::new();
445            let start = face.tri_start as usize;
446            let end = (start + face.tri_count as usize).min(solid.mesh.indices.len() / 3);
447            for tri in start..end {
448                let idx = [
449                    solid.mesh.indices[tri * 3],
450                    solid.mesh.indices[tri * 3 + 1],
451                    solid.mesh.indices[tri * 3 + 2],
452                ];
453                for k in 0..3 {
454                    let a = idx[k];
455                    let b = idx[(k + 1) % 3];
456                    let key = if a < b { (a, b) } else { (b, a) };
457                    *edge_use.entry(key).or_insert(0) += 1;
458                }
459            }
460            let mut boundary: Vec<(u32, u32)> = edge_use
461                .into_iter()
462                .filter_map(|(key, count)| (count == 1).then_some(key))
463                .collect();
464            boundary.sort_unstable();
465            for (a, b) in boundary {
466                segments.push(EdgeInstance {
467                    p0: solid.mesh.positions[a as usize],
468                    p1: solid.mesh.positions[b as usize],
469                });
470            }
471        }
472        let buf = (!segments.is_empty()).then(|| {
473            self.device
474                .create_buffer_init(&wgpu::util::BufferInitDescriptor {
475                    label: Some("emphasis boundary"),
476                    contents: bytemuck::cast_slice(&segments),
477                    usage: wgpu::BufferUsages::VERTEX,
478                })
479        });
480        gpu_solid.boundary = Some(BoundaryBuf {
481            emphasis_generation: emphasis.generation,
482            revision: solid.revision,
483            buf,
484            count: segments.len() as u32,
485        });
486    }
487}