Skip to main content

graph_explorer_render/
lib.rs

1mod camera;
2mod edges;
3mod halo;
4mod hit;
5mod labels;
6mod nodes;
7pub use camera::{Camera, CameraUniform};
8pub use hit::{contains, pick, HitTarget};
9pub use labels::LabelDraw;
10
11use edges::EdgePipeline;
12use halo::HaloPipeline;
13use labels::Labels;
14use nodes::NodePipeline;
15use wgpu::util::DeviceExt;
16
17/// One node to draw.
18#[derive(Debug, Clone, Copy, PartialEq)]
19pub struct NodeInstanceData {
20    pub pos: [f32; 2],
21    pub radius: f32,
22    pub opacity: f32,
23    pub shape: u32,      // 0 circle, 1 square, 2 diamond
24    pub color: [f32; 4],
25}
26
27/// One edge to draw (world-space endpoints).
28#[derive(Clone)]
29pub struct EdgeLineData {
30    pub a: [f32; 2],
31    pub b: [f32; 2],
32    pub color: [f32; 4],
33    pub width: f32,
34    pub opacity: f32,
35}
36
37/// One halo to draw (world-space, around a node).
38#[derive(Clone)]
39pub struct HaloInstanceData {
40    pub pos: [f32; 2],
41    pub radius: f32,
42    pub color: [f32; 4],
43    pub softness: f32,
44    /// Animation phase in `[0,1)`, advancing over time. Drives the rolling
45    /// wavefront in the halo shader (rings emanate outward as this increases).
46    pub phase: f32,
47}
48
49pub struct Renderer {
50    device: wgpu::Device,
51    queue: wgpu::Queue,
52    camera: Camera,
53    camera_buf: wgpu::Buffer,
54    camera_bind_group: wgpu::BindGroup,
55    nodes: NodePipeline,
56    /// World AABB of the current node set (`pos ± radius`), folded during
57    /// `set_nodes` so `fit_view` is O(1) and no per-frame copy of the
58    /// instance data is retained. `None` when the node set is empty.
59    node_bounds: Option<([f32; 2], [f32; 2])>,
60    edges: EdgePipeline,
61    halos: HaloPipeline,
62    labels: Labels,
63}
64
65impl Renderer {
66    pub fn new(device: wgpu::Device, queue: wgpu::Queue, format: wgpu::TextureFormat, width: u32, height: u32) -> Self {
67        let camera = Camera::new(width as f32, height as f32);
68        let camera_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
69            label: Some("camera"),
70            contents: bytemuck::bytes_of(&camera.uniform()),
71            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
72        });
73        let camera_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
74            label: Some("camera-bgl"),
75            entries: &[wgpu::BindGroupLayoutEntry {
76                binding: 0,
77                visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
78                ty: wgpu::BindingType::Buffer {
79                    ty: wgpu::BufferBindingType::Uniform,
80                    has_dynamic_offset: false,
81                    min_binding_size: None,
82                },
83                count: None,
84            }],
85        });
86        let camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
87            label: Some("camera-bg"),
88            layout: &camera_bgl,
89            entries: &[wgpu::BindGroupEntry { binding: 0, resource: camera_buf.as_entire_binding() }],
90        });
91
92        let nodes = NodePipeline::new(&device, format, &camera_bgl);
93        let edges = EdgePipeline::new(&device, format, &camera_bgl);
94        let halos = HaloPipeline::new(&device, format, &camera_bgl);
95        let labels = Labels::new(&device, &queue, format);
96
97        Self {
98            device,
99            queue,
100            camera,
101            camera_buf,
102            camera_bind_group,
103            nodes,
104            node_bounds: None,
105            edges,
106            halos,
107            labels,
108        }
109    }
110
111    pub fn resize(&mut self, width: u32, height: u32) {
112        self.camera.viewport = [width as f32, height as f32];
113    }
114
115    pub fn camera_mut(&mut self) -> &mut Camera { &mut self.camera }
116    pub fn camera_ref(&self) -> &Camera { &self.camera }
117
118    pub fn set_fade(&mut self, fade: f32) {
119        self.camera.fade = fade.clamp(0.0, 1.0);
120    }
121
122    /// Reconfigure a surface owned by the caller (e.g. `GraphView`) using this
123    /// renderer's device. `GraphView` owns `surface`/`config` but not the wgpu
124    /// `Device` (that lives inside `Renderer`), so surface reconfiguration on
125    /// resize is routed through here to keep device ownership in one place.
126    pub fn configure_surface(&self, surface: &wgpu::Surface, config: &wgpu::SurfaceConfiguration) {
127        surface.configure(&self.device, config);
128    }
129
130    pub fn set_nodes(&mut self, nodes: &[NodeInstanceData]) {
131        self.nodes.upload(&self.device, &self.queue, nodes);
132        // Cheap min/max fold over data the upload just walked (still in
133        // cache) — replaces retaining a full per-frame copy for fit_view.
134        self.node_bounds = if nodes.is_empty() {
135            None
136        } else {
137            let mut min = [f32::MAX, f32::MAX];
138            let mut max = [f32::MIN, f32::MIN];
139            for n in nodes {
140                min[0] = min[0].min(n.pos[0] - n.radius);
141                min[1] = min[1].min(n.pos[1] - n.radius);
142                max[0] = max[0].max(n.pos[0] + n.radius);
143                max[1] = max[1].max(n.pos[1] + n.radius);
144            }
145            Some((min, max))
146        };
147    }
148
149    /// Record this frame's label survivors; shapes only labels not already
150    /// in the shaped-buffer cache.
151    pub fn set_labels(&mut self, labels: &[LabelDraw]) {
152        self.labels.set_labels(labels);
153    }
154
155    pub fn set_edges(&mut self, edges: &[EdgeLineData]) {
156        self.edges.upload(&self.device, &self.queue, edges);
157    }
158
159    pub fn set_halos(&mut self, halos: &[HaloInstanceData]) {
160        self.halos.upload(&self.device, &self.queue, halos);
161    }
162
163    /// Advance the camera glide by `dt_ms` (paced by `dur_ms`).
164    pub fn tick(&mut self, dt_ms: f32, dur_ms: f32) {
165        self.camera.tick(dt_ms, dur_ms);
166    }
167
168    /// Glide the camera to frame world-AABB `[min,max]`.
169    pub fn glide_bounds(&mut self, min: [f32; 2], max: [f32; 2]) {
170        self.camera.glide_bounds(min, max);
171    }
172
173    /// Fit the camera to the current node set (including radii). No-op if empty.
174    pub fn fit_view(&mut self) {
175        if let Some((min, max)) = self.node_bounds {
176            self.camera.fit_bounds(min, max);
177        }
178    }
179
180    fn upload_camera(&self) {
181        self.queue.write_buffer(&self.camera_buf, 0, bytemuck::bytes_of(&self.camera.uniform()));
182    }
183
184    /// Clear the target, then draw edges beneath nodes, with labels layered on top.
185    pub fn render(&mut self, view: &wgpu::TextureView) -> Result<(), String> {
186        self.upload_camera();
187        self.labels.prepare(&self.device, &self.queue, &self.camera)?;
188        let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("frame") });
189        let label_res;
190        {
191            let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
192                label: Some("main-pass"),
193                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
194                    view,
195                    resolve_target: None,
196                    ops: wgpu::Operations {
197                        load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.05, g: 0.06, b: 0.09, a: 1.0 }),
198                        store: wgpu::StoreOp::Store,
199                    },
200                })],
201                depth_stencil_attachment: None,
202                timestamp_writes: None,
203                occlusion_query_set: None,
204            });
205            self.edges.draw(&mut rpass, &self.camera_bind_group);
206            self.halos.draw(&mut rpass, &self.camera_bind_group);
207            self.nodes.draw(&mut rpass, &self.camera_bind_group);
208            label_res = self.labels.draw(&mut rpass);
209        }
210        label_res?;
211        self.queue.submit(Some(encoder.finish()));
212        Ok(())
213    }
214}