1use super::ColorScheme;
7use bevy::prelude::*;
8use bon::Builder;
9use ferritin_core::AtomCollection;
10
11#[derive(Clone)]
13pub enum RenderOptions {
14 Wireframe,
15 Cartoon,
16 BallAndStick,
17 Solid,
18 Putty,
19}
20
21#[derive(Builder, Clone)]
23pub struct Structure {
24 pdb: AtomCollection,
25 #[builder(default = RenderOptions::Solid)]
26 rendertype: RenderOptions,
27 #[builder(default = ColorScheme::ByAtomType)]
28 color_scheme: ColorScheme,
29 #[builder(default = StandardMaterial::default())]
30 material: StandardMaterial,
31}
32
33impl Structure {
35 }
37
38impl Structure {
39 pub fn to_mesh(&self) -> Mesh {
41 match self.rendertype {
42 RenderOptions::Wireframe => self.render_wireframe(),
43 RenderOptions::Cartoon => self.render_cartoon(),
44 RenderOptions::BallAndStick => self.render_ballandstick(),
45 RenderOptions::Solid => self.render_spheres(),
46 RenderOptions::Putty => self.render_putty(),
47 }
48 }
49
50 pub fn get_material(&self) -> StandardMaterial {
52 self.material.clone()
53 }
54
55 fn render_wireframe(&self) -> Mesh {
57 self.create_sphere_mesh(0.5)
58 }
59
60 fn render_cartoon(&self) -> Mesh {
61 self.create_sphere_mesh(1.0)
62 }
63
64 fn render_ballandstick(&self) -> Mesh {
74 const ATOM_RADIUS: f32 = 0.4;
75 const BOND_RADIUS: f32 = 0.15;
76
77 let mut positions: Vec<[f32; 3]> = Vec::new();
78 let mut normals: Vec<[f32; 3]> = Vec::new();
79 let mut uvs: Vec<[f32; 2]> = Vec::new();
80 let mut indices: Vec<u32> = Vec::new();
81
82 for idx in 0..self.pdb.get_size() {
84 let coord = self.pdb.get_coord(idx);
85 let center = Vec3::new(coord[0], coord[1], coord[2]);
86 Self::append_sphere_geometry(
87 center,
88 ATOM_RADIUS,
89 &mut positions,
90 &mut normals,
91 &mut uvs,
92 &mut indices,
93 );
94 }
95
96 if let Some(bonds) = self.pdb.get_bonds() {
98 let coords = self.pdb.get_coords();
99 for bond in bonds.iter() {
100 let (a1, a2) = bond.get_atom_indices();
101 if let (Some(c1), Some(c2)) =
102 (coords.get(a1 as usize), coords.get(a2 as usize))
103 {
104 let p1 = Vec3::from_array(*c1);
105 let p2 = Vec3::from_array(*c2);
106 Self::append_cylinder_geometry(
107 p1,
108 p2,
109 BOND_RADIUS,
110 &mut positions,
111 &mut normals,
112 &mut uvs,
113 &mut indices,
114 );
115 }
116 }
117 }
118
119 let mut mesh = Mesh::new(
120 bevy::mesh::PrimitiveTopology::TriangleList,
121 bevy::asset::RenderAssetUsages::default(),
122 );
123 mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
124 mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals);
125 mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs);
126 mesh.insert_indices(bevy::mesh::Indices::U32(indices));
127 mesh
128 }
129
130 fn render_spheres(&self) -> Mesh {
137 const VDW_FALLBACK: f32 = 1.5;
138
139 let mut positions: Vec<[f32; 3]> = Vec::new();
140 let mut normals: Vec<[f32; 3]> = Vec::new();
141 let mut uvs: Vec<[f32; 2]> = Vec::new();
142 let mut indices: Vec<u32> = Vec::new();
143
144 for (coord, element) in self.pdb.iter_coords_and_elements() {
145 let center = Vec3::new(coord[0], coord[1], coord[2]);
146 let radius = element
147 .atomic_radius()
148 .van_der_waals
149 .map(|r| r as f32)
150 .unwrap_or(VDW_FALLBACK);
151 Self::append_sphere_geometry(
152 center,
153 radius,
154 &mut positions,
155 &mut normals,
156 &mut uvs,
157 &mut indices,
158 );
159 }
160
161 let mut mesh = Mesh::new(
162 bevy::mesh::PrimitiveTopology::TriangleList,
163 bevy::asset::RenderAssetUsages::default(),
164 );
165 mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
166 mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals);
167 mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs);
168 mesh.insert_indices(bevy::mesh::Indices::U32(indices));
169 mesh
170 }
171
172 fn render_putty(&self) -> Mesh {
173 self.create_sphere_mesh(2.0)
174 }
175
176 fn append_sphere_geometry(
181 center: Vec3,
182 radius: f32,
183 positions: &mut Vec<[f32; 3]>,
184 normals: &mut Vec<[f32; 3]>,
185 uvs: &mut Vec<[f32; 2]>,
186 indices: &mut Vec<u32>,
187 ) {
188 let subdivisions: u32 = 8;
189 let base_index = positions.len() as u32;
190
191 for lat in 0..=subdivisions {
192 let theta = lat as f32 * std::f32::consts::PI / subdivisions as f32;
193 let sin_theta = theta.sin();
194 let cos_theta = theta.cos();
195
196 for lon in 0..=subdivisions {
197 let phi = lon as f32 * 2.0 * std::f32::consts::PI / subdivisions as f32;
198 let x = sin_theta * phi.cos();
199 let y = cos_theta;
200 let z = sin_theta * phi.sin();
201
202 let normal = Vec3::new(x, y, z);
203 let pos = center + normal * radius;
204
205 positions.push([pos.x, pos.y, pos.z]);
206 normals.push([normal.x, normal.y, normal.z]);
207 uvs.push([
208 lon as f32 / subdivisions as f32,
209 lat as f32 / subdivisions as f32,
210 ]);
211 }
212 }
213
214 for lat in 0..subdivisions {
215 for lon in 0..subdivisions {
216 let first = base_index + lat * (subdivisions + 1) + lon;
217 let second = first + subdivisions + 1;
218
219 indices.push(first);
220 indices.push(second);
221 indices.push(first + 1);
222
223 indices.push(second);
224 indices.push(second + 1);
225 indices.push(first + 1);
226 }
227 }
228 }
229
230 fn append_cylinder_geometry(
236 p1: Vec3,
237 p2: Vec3,
238 radius: f32,
239 positions: &mut Vec<[f32; 3]>,
240 normals: &mut Vec<[f32; 3]>,
241 uvs: &mut Vec<[f32; 2]>,
242 indices: &mut Vec<u32>,
243 ) {
244 let direction = p2 - p1;
245 let length = direction.length();
246 if length < 1e-6 {
247 return;
248 }
249 let axis = direction.normalize();
250
251 let right = if axis.abs_diff_eq(Vec3::Y, 0.01) {
253 axis.cross(Vec3::Z).normalize()
254 } else {
255 axis.cross(Vec3::Y).normalize()
256 };
257 let up = axis.cross(right).normalize();
258
259 const SEGMENTS: u32 = 8;
260 let base_index = positions.len() as u32;
261
262 for ring in 0..=1u32 {
264 let center = if ring == 0 { p1 } else { p2 };
265 let v = ring as f32; for seg in 0..=SEGMENTS {
268 let angle = seg as f32 * 2.0 * std::f32::consts::PI / SEGMENTS as f32;
269 let normal = (right * angle.cos() + up * angle.sin()).normalize();
270 let pos = center + normal * radius;
271
272 positions.push([pos.x, pos.y, pos.z]);
273 normals.push([normal.x, normal.y, normal.z]);
274 uvs.push([seg as f32 / SEGMENTS as f32, v]);
275 }
276 }
277
278 let ring_verts = SEGMENTS + 1;
280 for seg in 0..SEGMENTS {
281 let i00 = base_index + seg;
282 let i01 = base_index + seg + 1;
283 let i10 = base_index + ring_verts + seg;
284 let i11 = base_index + ring_verts + seg + 1;
285
286 indices.push(i00);
287 indices.push(i10);
288 indices.push(i01);
289
290 indices.push(i01);
291 indices.push(i10);
292 indices.push(i11);
293 }
294 }
295
296 fn create_sphere_mesh(&self, radius: f32) -> Mesh {
301 let mut positions = Vec::new();
302 let mut normals = Vec::new();
303 let mut uvs = Vec::new();
304 let mut indices = Vec::new();
305
306 let subdivisions = 8;
307
308 for idx in 0..self.pdb.get_size() {
309 let coord = self.pdb.get_coord(idx);
310 let center = Vec3::new(coord[0], coord[1], coord[2]);
311 let base_index = positions.len() as u32;
312
313 for lat in 0..=subdivisions {
315 let theta = lat as f32 * std::f32::consts::PI / subdivisions as f32;
316 let sin_theta = theta.sin();
317 let cos_theta = theta.cos();
318
319 for lon in 0..=subdivisions {
320 let phi = lon as f32 * 2.0 * std::f32::consts::PI / subdivisions as f32;
321 let sin_phi = phi.sin();
322 let cos_phi = phi.cos();
323
324 let x = sin_theta * cos_phi;
325 let y = cos_theta;
326 let z = sin_theta * sin_phi;
327
328 let normal = Vec3::new(x, y, z);
329 let pos = center + normal * radius;
330
331 positions.push([pos.x, pos.y, pos.z]);
332 normals.push([normal.x, normal.y, normal.z]);
333 uvs.push([
334 lon as f32 / subdivisions as f32,
335 lat as f32 / subdivisions as f32,
336 ]);
337 }
338 }
339
340 for lat in 0..subdivisions {
342 for lon in 0..subdivisions {
343 let first = base_index + lat * (subdivisions + 1) + lon;
344 let second = first + subdivisions + 1;
345
346 indices.push(first);
347 indices.push(second);
348 indices.push(first + 1);
349
350 indices.push(second);
351 indices.push(second + 1);
352 indices.push(first + 1);
353 }
354 }
355 }
356
357 let mut mesh = Mesh::new(
359 bevy::mesh::PrimitiveTopology::TriangleList,
360 bevy::asset::RenderAssetUsages::default(),
361 );
362
363 mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
364 mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals);
365 mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs);
366 mesh.insert_indices(bevy::mesh::Indices::U32(indices));
367
368 mesh
369 }
370}
371
372#[cfg(test)]
385mod tests {
386 use super::*;
387 use bevy::mesh::Indices;
388 use bevy::render::mesh::VertexAttributeValues;
389 use ferritin_core::load_structure;
390 use ferritin_test_data::TestFile;
391
392 fn load_test_structure() -> anyhow::Result<Structure> {
393 let (molfile, _handle) = TestFile::protein_01().create_temp()?;
394 let ac = load_structure(molfile)?;
395 Ok(Structure::builder().pdb(ac).build())
396 }
397
398 fn assert_mesh_has_required_attributes(mesh: &Mesh, label: &str) {
399 assert!(
400 mesh.attribute(Mesh::ATTRIBUTE_POSITION).is_some(),
401 "{label}: missing ATTRIBUTE_POSITION"
402 );
403 assert!(
404 mesh.attribute(Mesh::ATTRIBUTE_NORMAL).is_some(),
405 "{label}: missing ATTRIBUTE_NORMAL"
406 );
407 assert!(
408 mesh.attribute(Mesh::ATTRIBUTE_UV_0).is_some(),
409 "{label}: missing ATTRIBUTE_UV_0"
410 );
411 assert!(
412 mesh.indices().is_some(),
413 "{label}: missing indices"
414 );
415 }
416
417 fn assert_indices_are_u32(mesh: &Mesh, label: &str) {
418 match mesh.indices() {
419 Some(Indices::U32(_)) => {}
420 Some(Indices::U16(_)) => panic!("{label}: expected U32 indices, got U16"),
421 None => panic!("{label}: no indices"),
422 }
423 }
424
425 fn assert_no_color_attribute(mesh: &Mesh, label: &str) {
426 assert!(
427 mesh.attribute(Mesh::ATTRIBUTE_COLOR).is_none(),
428 "{label}: ATTRIBUTE_COLOR should not be populated"
429 );
430 }
431
432 fn assert_positions_are_float32x3(mesh: &Mesh, label: &str) {
433 match mesh.attribute(Mesh::ATTRIBUTE_POSITION) {
434 Some(VertexAttributeValues::Float32x3(_)) => {}
435 other => panic!("{label}: ATTRIBUTE_POSITION is {other:?}, expected Float32x3"),
436 }
437 }
438
439 fn assert_normals_are_float32x3(mesh: &Mesh, label: &str) {
440 match mesh.attribute(Mesh::ATTRIBUTE_NORMAL) {
441 Some(VertexAttributeValues::Float32x3(_)) => {}
442 other => panic!("{label}: ATTRIBUTE_NORMAL is {other:?}, expected Float32x3"),
443 }
444 }
445
446 fn assert_uvs_are_float32x2(mesh: &Mesh, label: &str) {
447 match mesh.attribute(Mesh::ATTRIBUTE_UV_0) {
448 Some(VertexAttributeValues::Float32x2(_)) => {}
449 other => panic!("{label}: ATTRIBUTE_UV_0 is {other:?}, expected Float32x2"),
450 }
451 }
452
453 #[test]
454 fn test_render_solid_attributes() -> anyhow::Result<()> {
455 let s = load_test_structure()?;
456 let mesh = s.to_mesh(); assert_mesh_has_required_attributes(&mesh, "render_solid");
458 assert_indices_are_u32(&mesh, "render_solid");
459 assert_positions_are_float32x3(&mesh, "render_solid");
460 assert_normals_are_float32x3(&mesh, "render_solid");
461 assert_uvs_are_float32x2(&mesh, "render_solid");
462 assert_no_color_attribute(&mesh, "render_solid");
463 assert!(mesh.count_vertices() > 0, "render_solid: no vertices");
464 Ok(())
465 }
466
467 #[test]
468 fn test_render_wireframe_attributes() -> anyhow::Result<()> {
469 let (molfile, _handle) = TestFile::protein_01().create_temp()?;
470 let ac = load_structure(molfile)?;
471 let s = Structure::builder().pdb(ac).rendertype(RenderOptions::Wireframe).build();
472 let mesh = s.to_mesh();
473 assert_mesh_has_required_attributes(&mesh, "render_wireframe");
474 assert_indices_are_u32(&mesh, "render_wireframe");
475 assert_positions_are_float32x3(&mesh, "render_wireframe");
476 assert_normals_are_float32x3(&mesh, "render_wireframe");
477 assert_uvs_are_float32x2(&mesh, "render_wireframe");
478 assert_no_color_attribute(&mesh, "render_wireframe");
479 Ok(())
480 }
481
482 #[test]
483 fn test_render_cartoon_attributes() -> anyhow::Result<()> {
484 let (molfile, _handle) = TestFile::protein_01().create_temp()?;
485 let ac = load_structure(molfile)?;
486 let s = Structure::builder().pdb(ac).rendertype(RenderOptions::Cartoon).build();
487 let mesh = s.to_mesh();
488 assert_mesh_has_required_attributes(&mesh, "render_cartoon");
489 assert_indices_are_u32(&mesh, "render_cartoon");
490 assert_positions_are_float32x3(&mesh, "render_cartoon");
491 assert_normals_are_float32x3(&mesh, "render_cartoon");
492 assert_uvs_are_float32x2(&mesh, "render_cartoon");
493 assert_no_color_attribute(&mesh, "render_cartoon");
494 Ok(())
495 }
496
497 #[test]
498 fn test_render_ballandstick_attributes() -> anyhow::Result<()> {
499 let (molfile, _handle) = TestFile::protein_01().create_temp()?;
500 let ac = load_structure(molfile)?;
501 let s = Structure::builder().pdb(ac).rendertype(RenderOptions::BallAndStick).build();
502 let mesh = s.to_mesh();
503 assert_mesh_has_required_attributes(&mesh, "render_ballandstick");
504 assert_indices_are_u32(&mesh, "render_ballandstick");
505 assert_positions_are_float32x3(&mesh, "render_ballandstick");
506 assert_normals_are_float32x3(&mesh, "render_ballandstick");
507 assert_uvs_are_float32x2(&mesh, "render_ballandstick");
508 assert_no_color_attribute(&mesh, "render_ballandstick");
509 Ok(())
510 }
511
512 #[test]
513 fn test_render_putty_attributes() -> anyhow::Result<()> {
514 let (molfile, _handle) = TestFile::protein_01().create_temp()?;
515 let ac = load_structure(molfile)?;
516 let s = Structure::builder().pdb(ac).rendertype(RenderOptions::Putty).build();
517 let mesh = s.to_mesh();
518 assert_mesh_has_required_attributes(&mesh, "render_putty");
519 assert_indices_are_u32(&mesh, "render_putty");
520 assert_positions_are_float32x3(&mesh, "render_putty");
521 assert_normals_are_float32x3(&mesh, "render_putty");
522 assert_uvs_are_float32x2(&mesh, "render_putty");
523 assert_no_color_attribute(&mesh, "render_putty");
524 Ok(())
525 }
526
527 #[test]
528 fn test_vertex_and_index_counts_are_consistent() -> anyhow::Result<()> {
529 let s = load_test_structure()?;
530 let mesh = s.to_mesh();
531 let n_verts = mesh.count_vertices();
532 assert!(n_verts > 0, "mesh has no vertices");
533 if let Some(Indices::U32(idx)) = mesh.indices() {
534 assert!(!idx.is_empty(), "mesh has no indices");
535 assert!(
536 idx.iter().all(|&i| (i as usize) < n_verts),
537 "index out of bounds"
538 );
539 }
540 Ok(())
541 }
542
543 #[test]
544 fn test_render_ballandstick_has_more_verts_than_solid() -> anyhow::Result<()> {
545 let (molfile, _handle) = TestFile::protein_01().create_temp()?;
554 let ac = load_structure(molfile)?;
555 let n_atoms = ac.get_size();
556 let has_bonds = ac.get_bonds().map(|b| !b.is_empty()).unwrap_or(false);
557
558 let s = Structure::builder()
559 .pdb(ac)
560 .rendertype(RenderOptions::BallAndStick)
561 .build();
562 let mesh = s.to_mesh();
563
564 let sphere_only_verts = n_atoms * 81;
566 assert!(
567 mesh.count_vertices() >= sphere_only_verts,
568 "expected at least {} vertices from atom spheres, got {}",
569 sphere_only_verts,
570 mesh.count_vertices(),
571 );
572
573 if has_bonds {
574 assert!(
575 mesh.count_vertices() > sphere_only_verts,
576 "expected bond cylinder vertices on top of atom spheres: total={}, spheres_only={}",
577 mesh.count_vertices(),
578 sphere_only_verts,
579 );
580 }
581 Ok(())
582 }
583
584 #[test]
585 fn test_render_solid_uses_vdw_radii() -> anyhow::Result<()> {
586 let s = load_test_structure()?;
590 let mesh = s.to_mesh();
591 assert!(mesh.count_vertices() > 0, "render_solid: no vertices");
592 Ok(())
593 }
594}