Skip to main content

graph_explorer_render/
lib.rs

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