Skip to main content

brep_render/render/
scene_sync.rs

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