BREP_render 0.1.0

BREP Rust rendering engine: kernel-fed scene store + wgpu renderer (headless artifact, desktop window, and wasm canvas shells).
Documentation
use super::*;

impl RenderCore {
    /// The base face color of one solid under the active settings, in LINEAR
    /// space (converted with f64 precision so artifact bytes stay stable).
    fn face_base_color(solid: &SolidDisplay, settings: &RenderSettings) -> Rgba {
        if let Some(rgb) = solid.color_override {
            return [
                srgb_to_linear(rgb[0] as f64) as f32,
                srgb_to_linear(rgb[1] as f64) as f32,
                srgb_to_linear(rgb[2] as f64) as f32,
                1.0,
            ];
        }
        match settings.face_color_mode {
            FaceColorMode::HashedBySolid => {
                let srgb = solid_color_srgb(&solid.name);
                [
                    srgb_to_linear(srgb[0]) as f32,
                    srgb_to_linear(srgb[1]) as f32,
                    srgb_to_linear(srgb[2]) as f32,
                    1.0,
                ]
            }
            FaceColorMode::Uniform => {
                let c = settings.face_color;
                [
                    srgb_to_linear(c[0] as f64) as f32,
                    srgb_to_linear(c[1] as f64) as f32,
                    srgb_to_linear(c[2] as f64) as f32,
                    c[3],
                ]
            }
        }
    }

    fn upload_solid(&self, solid: &SolidDisplay, settings: &RenderSettings) -> GpuSolid {
        let vertices: Vec<MeshVertex> = solid
            .mesh
            .positions
            .iter()
            .zip(&solid.mesh.normals)
            .map(|(&position, &normal)| MeshVertex { position, normal })
            .collect();
        let vertex_buf = self
            .device
            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
                label: Some(solid.name.as_str()),
                contents: bytemuck::cast_slice(&vertices),
                usage: wgpu::BufferUsages::VERTEX,
            });
        let index_buf = self
            .device
            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
                label: Some(solid.name.as_str()),
                contents: bytemuck::cast_slice(&solid.mesh.indices),
                usage: wgpu::BufferUsages::INDEX,
            });
        // Wireframe line-list: every triangle (a,b,c) → edges (a,b),(b,c),(c,a).
        let mut wire_indices: Vec<u32> = Vec::with_capacity(solid.mesh.indices.len() * 2);
        for tri in solid.mesh.indices.chunks_exact(3) {
            wire_indices.extend_from_slice(&[
                tri[0], tri[1], tri[1], tri[2], tri[2], tri[0],
            ]);
        }
        let wire_index_count = wire_indices.len() as u32;
        let wire_index_buf = self
            .device
            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
                label: Some(solid.name.as_str()),
                contents: bytemuck::cast_slice(&wire_indices),
                usage: wgpu::BufferUsages::INDEX,
            });
        let faces = solid
            .faces
            .iter()
            .map(|face| FaceRange {
                first_index: face.tri_start * 3,
                index_count: face.tri_count * 3,
            })
            .collect();

        let mut segments: Vec<EdgeInstance> = Vec::new();
        let mut edges = Vec::with_capacity(solid.edges.len());
        for edge in &solid.edges {
            let first_instance = segments.len() as u32;
            for pair in edge.polyline.windows(2) {
                segments.push(EdgeInstance {
                    p0: pair[0],
                    p1: pair[1],
                });
            }
            edges.push(EdgeRange {
                first_instance,
                instance_count: segments.len() as u32 - first_instance,
            });
        }
        let edge_buf = (!segments.is_empty()).then(|| {
            self.device
                .create_buffer_init(&wgpu::util::BufferInitDescriptor {
                    label: Some(solid.name.as_str()),
                    contents: bytemuck::cast_slice(&segments),
                    usage: wgpu::BufferUsages::VERTEX,
                })
        });

        let points: Vec<PointInstance> = solid
            .vertices
            .iter()
            .map(|v| PointInstance {
                center: [
                    v.position[0] as f32,
                    v.position[1] as f32,
                    v.position[2] as f32,
                ],
            })
            .collect();
        let point_buf = (!points.is_empty()).then(|| {
            self.device
                .create_buffer_init(&wgpu::util::BufferInitDescriptor {
                    label: Some(solid.name.as_str()),
                    contents: bytemuck::cast_slice(&points),
                    usage: wgpu::BufferUsages::VERTEX,
                })
        });

        let base_style = StyleBuf::new(&self.device, &self.style_layout, solid.name.as_str());
        base_style.write(
            &self.queue,
            Self::face_base_color(solid, settings),
            [if settings.flat_shading { 1.0 } else { 0.0 }, 0.0, 0.0, 0.0],
        );

        GpuSolid {
            revision: solid.revision,
            vertex_buf,
            index_buf,
            wire_index_buf,
            wire_index_count,
            faces,
            edge_buf,
            edges,
            edge_instances: segments.len() as u32,
            point_buf,
            point_count: points.len() as u32,
            base_style,
            boundary: None,
        }
    }

    /// Bring the GPU scene in line with the display scene: new/changed solids
    /// upload, unchanged solids keep their buffers (R10), removed solids drop
    /// theirs. `settings_generation` invalidates per-solid style buffers only.
    pub fn sync_scene(
        &self,
        gpu: &mut GpuScene,
        scene: &RenderScene,
        settings: &RenderSettings,
        settings_generation: u64,
    ) {
        gpu.order.clear();
        let mut seen: Vec<&str> = Vec::with_capacity(scene.solids().len());
        for solid in scene.solids() {
            gpu.order.push(solid.name.clone());
            seen.push(solid.name.as_str());
            let refresh_style = gpu.settings_generation != settings_generation;
            match gpu.solids.get_mut(&solid.name) {
                Some(existing) if existing.revision == solid.revision => {
                    if refresh_style {
                        existing.base_style.write(
                            &self.queue,
                            Self::face_base_color(solid, settings),
                            [if settings.flat_shading { 1.0 } else { 0.0 }, 0.0, 0.0, 0.0],
                        );
                    }
                }
                _ => {
                    let uploaded = self.upload_solid(solid, settings);
                    gpu.solids.insert(solid.name.clone(), uploaded);
                    gpu.uploads += 1;
                }
            }
        }
        gpu.solids.retain(|name, _| seen.contains(&name.as_str()));
        gpu.settings_generation = settings_generation;
    }

    /// Upload a scene from scratch (artifact/one-shot path).
    pub fn upload_scene(&self, scene: &RenderScene) -> GpuScene {
        self.upload_scene_with(scene, &RenderSettings::artifact())
    }

    pub fn upload_scene_with(&self, scene: &RenderScene, settings: &RenderSettings) -> GpuScene {
        let mut gpu = GpuScene::default();
        self.sync_scene(&mut gpu, scene, settings, 0);
        gpu
    }

    pub(super) fn ensure_targets(&mut self, width: u32, height: u32) {
        if let Some(targets) = &self.targets {
            if targets.width == width && targets.height == height {
                return;
            }
        }
        let size = wgpu::Extent3d {
            width,
            height,
            depth_or_array_layers: 1,
        };
        let msaa = self.device.create_texture(&wgpu::TextureDescriptor {
            label: Some("msaa color"),
            size,
            mip_level_count: 1,
            sample_count: SAMPLES,
            dimension: wgpu::TextureDimension::D2,
            format: self.format,
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
            view_formats: &[],
        });
        let depth = self.device.create_texture(&wgpu::TextureDescriptor {
            label: Some("depth"),
            size,
            mip_level_count: 1,
            sample_count: SAMPLES,
            dimension: wgpu::TextureDimension::D2,
            format: DEPTH_FORMAT,
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
            view_formats: &[],
        });
        self.targets = Some(CachedTargets {
            width,
            height,
            msaa_view: msaa.create_view(&Default::default()),
            depth_view: depth.create_view(&Default::default()),
        });
    }

    pub(super) fn write_global_styles(&self, settings: &RenderSettings) {
        let flat = if settings.flat_shading { 1.0 } else { 0.0 };
        let face = |c: Rgba| -> Rgba {
            [
                srgb_to_linear(c[0] as f64) as f32,
                srgb_to_linear(c[1] as f64) as f32,
                srgb_to_linear(c[2] as f64) as f32,
                c[3],
            ]
        };
        let s = &self.styles;
        let q = &self.queue;
        s.face_selected
            .write(q, face(settings.face_selected_color), [flat, 0.0, 0.0, 0.0]);
        s.face_hovered
            .write(q, face(settings.hover_color), [flat, 0.0, 0.0, 0.0]);
        let edge_params = [settings.edge_width_px, EDGE_NUDGE, 0.0, 0.0];
        s.edge_base.write(q, settings.edge_color, edge_params);
        s.edge_selected.write(q, settings.edge_selected_color, edge_params);
        s.edge_hovered.write(q, settings.hover_color, edge_params);
        let mut hidden = settings.edge_color;
        hidden[3] = settings.hidden_edge_alpha;
        s.edge_hidden.write(q, hidden, edge_params);
        // Selected-face boundary: selected-edge color, slightly wider, extra
        // nudge so it reads as an outline on top of the fill.
        s.boundary.write(
            q,
            settings.edge_selected_color,
            [settings.edge_width_px + 1.0, EDGE_NUDGE * 1.5, 0.0, 0.0],
        );
        let point_params = [settings.vertex_size_px, 0.0, 0.0, 0.0];
        s.point_base.write(q, settings.vertex_color, point_params);
        s.point_selected
            .write(q, settings.vertex_selected_color, [settings.vertex_size_px + 1.0, 0.0, 0.0, 0.0]);
        s.point_hovered
            .write(q, settings.hover_color, [settings.vertex_size_px + 1.0, 0.0, 0.0, 0.0]);
        let axis_params = [2.0, EDGE_NUDGE, 0.0, 0.0];
        s.axis_x.write(q, [0.91, 0.30, 0.32, 1.0], axis_params);
        s.axis_y.write(q, [0.27, 0.80, 0.42, 1.0], axis_params);
        s.axis_z.write(q, [0.23, 0.51, 0.96, 1.0], axis_params);
        // Overlay-widget line width (color is per-instance from the gizmo).
        s.overlay_line.write(q, [1.0, 1.0, 1.0, 1.0], [2.5, 0.0, 0.0, 0.0]);
    }

    /// Rebuild the selected/hovered-face boundary outline buffer for one solid
    /// when the emphasis or the solid changed. The boundary of a face's
    /// triangle range = mesh edges used an odd number of times inside it.
    pub(super) fn sync_boundary(&self, gpu_solid: &mut GpuSolid, solid: &SolidDisplay, emphasis: &Emphasis) {
        if let Some(boundary) = &gpu_solid.boundary {
            if boundary.emphasis_generation == emphasis.generation
                && boundary.revision == solid.revision
            {
                return;
            }
        }
        let mut segments: Vec<EdgeInstance> = Vec::new();
        for face in &solid.faces {
            if face.tri_count == 0 {
                continue;
            }
            let state = emphasis.face_state(&solid.name, &face.name);
            if state == EmphasisState::Base {
                continue;
            }
            let mut edge_use: HashMap<(u32, u32), u32> = HashMap::new();
            let start = face.tri_start as usize;
            let end = (start + face.tri_count as usize).min(solid.mesh.indices.len() / 3);
            for tri in start..end {
                let idx = [
                    solid.mesh.indices[tri * 3],
                    solid.mesh.indices[tri * 3 + 1],
                    solid.mesh.indices[tri * 3 + 2],
                ];
                for k in 0..3 {
                    let a = idx[k];
                    let b = idx[(k + 1) % 3];
                    let key = if a < b { (a, b) } else { (b, a) };
                    *edge_use.entry(key).or_insert(0) += 1;
                }
            }
            let mut boundary: Vec<(u32, u32)> = edge_use
                .into_iter()
                .filter_map(|(key, count)| (count == 1).then_some(key))
                .collect();
            boundary.sort_unstable();
            for (a, b) in boundary {
                segments.push(EdgeInstance {
                    p0: solid.mesh.positions[a as usize],
                    p1: solid.mesh.positions[b as usize],
                });
            }
        }
        let buf = (!segments.is_empty()).then(|| {
            self.device
                .create_buffer_init(&wgpu::util::BufferInitDescriptor {
                    label: Some("emphasis boundary"),
                    contents: bytemuck::cast_slice(&segments),
                    usage: wgpu::BufferUsages::VERTEX,
                })
        });
        gpu_solid.boundary = Some(BoundaryBuf {
            emphasis_generation: emphasis.generation,
            revision: solid.revision,
            buf,
            count: segments.len() as u32,
        });
    }
}