hoomd_bevy/representation/
hyperbolic_disk.rs

1// Copyright (c) 2024-2026 The Regents of the University of Michigan.
2// Part of hoomd-rs, released under the BSD 3-Clause License.
3
4//! Implement Hyperbolic disks in Bevy.
5
6use crate::PRIMARY_COLOR;
7use bevy::{
8    asset::embedded_asset,
9    image::TRANSPARENT_IMAGE_HANDLE,
10    prelude::*,
11    reflect::TypePath,
12    render::render_resource::AsBindGroup,
13    shader::ShaderRef,
14    sprite_render::{AlphaMode2d, Material2d, Material2dPlugin},
15};
16use hoomd_manifold::{Hyperbolic, Minkowski};
17use itertools::{
18    EitherOrBoth::{Both, Left, Right},
19    Itertools,
20};
21use std::marker::PhantomData;
22
23/// Location of the shader implementation
24const SHADER_ASSET_PATH: &str = "embedded://hoomd_bevy/representation/hyperbolic_disk.wgsl";
25
26/// Represent an entity with a 2D disk in the xy plane.
27///
28/// The base representation has a diameter of 1.0. Provide a non-unit diameter in
29/// [`sync`] to render disks of different sizes. Nominally, the z coordinate of the
30/// disks should be set to 0. Choose a different value to control the back to front
31/// draw order.
32///
33/// All disks of the same type must have the same material. To display disks with
34/// different colors, outline widths, or textures, `setup` and `sync` multiple types
35/// of disks with different marker types.
36///
37/// To use:
38/// * Add [`setup`] to the `Startup` schedule.
39/// * Call [`sync`] in an `Update` schedule that runs after `AdvanceSet`.
40///
41/// [`setup`]: Self::setup
42/// [`sync`]: Self::sync
43#[derive(Component)]
44pub struct HyperbolicDisk<T> {
45    /// Mark the type of the disk.
46    marker: PhantomData<T>,
47}
48
49/// Assets that represent a Disk in the scene.
50#[derive(Resource)]
51pub struct HyperbolicDiskAssets<T> {
52    /// The disk mesh.
53    mesh: Handle<Mesh>,
54    /// The disk material.
55    material: Handle<HyperbolicDiskMaterial>,
56    /// Mark the type of the disk assets.
57    marker: PhantomData<T>,
58}
59
60/// Initialize needed plugins and add assets for this representation.
61pub(crate) fn build(app: &mut App) {
62    app.add_plugins(Material2dPlugin::<HyperbolicDiskMaterial>::default());
63    embedded_asset!(app, "hyperbolic_disk.wgsl");
64}
65
66impl<T: Send + Sync + 'static> HyperbolicDisk<T> {
67    /// Create assets to render disks.
68    pub fn setup(
69        material: In<HyperbolicDiskMaterial>,
70        mut commands: Commands,
71        mut meshes: ResMut<Assets<Mesh>>,
72        mut materials: ResMut<Assets<HyperbolicDiskMaterial>>,
73    ) {
74        let mesh = meshes.add(Rectangle::new(1.0, 1.0));
75        let material = materials.add(material.0);
76        commands.insert_resource(HyperbolicDiskAssets::<T> {
77            mesh,
78            material,
79            marker: PhantomData,
80        });
81    }
82
83    /// Copy the current positions of simulation particles to bevy entities.
84    pub fn sync<I>(
85        commands: &mut Commands,
86        disk_assets: Res<HyperbolicDiskAssets<T>>,
87        query: Query<(Entity, &mut Transform), With<Self>>,
88        disks: I,
89    ) where
90        I: IntoIterator<Item = (Minkowski<3>, f64)>,
91    {
92        for item in &mut query.into_iter().zip_longest(disks) {
93            match item {
94                Both((_, mut transform), (position, diameter)) => {
95                    let (poincare_position, max_projected_radius) = poincare(&position, diameter);
96                    let rad_arg = (diameter / 2.0).sinh() / (1.0 + (diameter / 2.0).cosh());
97                    let poincare_radius = (0.5)
98                        * (1.0 + 2.0 * rad_arg.powi(2) / (1.0 - (rad_arg.powi(2)))).acosh() as f32;
99                    transform.translation = Vec3::from_array(poincare_position);
100                    // transform.scale = Vec3::splat(1.0);
101                    transform.scale = Vec3::from_array([
102                        max_projected_radius,
103                        max_projected_radius,
104                        poincare_radius,
105                    ]);
106                }
107                Left((entity, _)) => commands.entity(entity).despawn(),
108                Right((position, diameter)) => {
109                    let (poincare_position, max_projected_radius) = poincare(&position, diameter);
110                    let rad_arg = (diameter / 2.0).sinh() / (1.0 + (diameter / 2.0).cosh());
111                    let poincare_radius = (0.5)
112                        * (1.0 + 2.0 * rad_arg.powi(2) / (1.0 - (rad_arg.powi(2)))).acosh() as f32;
113                    commands.spawn((
114                        Mesh2d(disk_assets.mesh.clone()),
115                        MeshMaterial2d(disk_assets.material.clone()),
116                        Transform::from_translation(Vec3::from_array(poincare_position))
117                            .with_scale(Vec3::from_array([
118                                max_projected_radius,
119                                max_projected_radius,
120                                poincare_radius,
121                            ])),
122                        Self {
123                            marker: PhantomData,
124                        },
125                    ));
126                }
127            }
128        }
129    }
130}
131
132/// Project coordinates to Poincaré disk
133fn poincare(point: &Minkowski<3>, diameter: f64) -> ([f32; 3], f32) {
134    let pt = Hyperbolic::from_minkowski_coordinates(*point);
135    let proj = pt.to_poincare();
136    let v = diameter / 2.0;
137    let eta = (point.coordinates[2]).acosh();
138    let edge_proj = ((eta - v).sinh()) / (1.0 + (eta - v).cosh());
139    let rad_proj = ((eta).sinh()) / (1.0 + (eta).cosh()) - edge_proj;
140    ([proj[0] as f32, proj[1] as f32, 0.0_f32], rad_proj as f32)
141}
142
143/// Control how disks are rendered.
144///
145/// [`HyperbolicDiskMaterial`] mixes the texture (which defaults to fully transparent) with
146/// the background color using the texture alpha. It ignores the background alpha.
147///
148/// Control the draw order using the z coordinate. The draw order is non-deterministic
149/// for all disks at the same z value.
150#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
151pub struct HyperbolicDiskMaterial {
152    /// Color applied to the interior of the disk.
153    #[uniform(0)]
154    pub background_color: LinearRgba,
155
156    /// Color applied to the outline.
157    #[uniform(1)]
158    pub outline_color: LinearRgba,
159
160    /// Width of the outline.
161    #[uniform(2)]
162    pub outline_width: f32,
163
164    /// Factor to scale the texture by.
165    #[uniform(3)]
166    pub texture_scale: f32,
167
168    /// Texture to apply. Blended with `color`.
169    #[texture(4)]
170    #[sampler(5)]
171    pub texture: Handle<Image>,
172}
173
174impl Default for HyperbolicDiskMaterial {
175    fn default() -> Self {
176        Self {
177            background_color: PRIMARY_COLOR.into(),
178            outline_color: Color::linear_rgb(0.0, 0.0, 0.0).into(),
179            outline_width: 0.005,
180            texture: TRANSPARENT_IMAGE_HANDLE,
181            texture_scale: 1.2,
182        }
183    }
184}
185
186impl HyperbolicDiskMaterial {
187    /// color for ghost particles
188    #[must_use]
189    pub fn ghost() -> Self {
190        Self {
191            background_color: Color::linear_rgb(0.5, 0.5, 0.5).into(),
192            outline_color: Color::linear_rgb(0.0, 0.0, 0.0).into(),
193            outline_width: 0.005,
194            texture: TRANSPARENT_IMAGE_HANDLE,
195            texture_scale: 1.2,
196        }
197    }
198}
199
200impl Material2d for HyperbolicDiskMaterial {
201    fn fragment_shader() -> ShaderRef {
202        SHADER_ASSET_PATH.into()
203    }
204
205    fn vertex_shader() -> ShaderRef {
206        SHADER_ASSET_PATH.into()
207    }
208
209    fn alpha_mode(&self) -> AlphaMode2d {
210        AlphaMode2d::Mask(0.5)
211    }
212}