Skip to main content

cloudiful_bevy_outline/
lib.rs

1use bevy::ecs::hierarchy::ChildSpawnerCommands;
2use bevy::prelude::*;
3use bevy::render::render_resource::Face;
4
5pub struct GpmoOutlinePlugin {
6    default_style: OutlineStyle,
7}
8
9impl Default for GpmoOutlinePlugin {
10    fn default() -> Self {
11        Self {
12            default_style: OutlineStyle::default(),
13        }
14    }
15}
16
17impl GpmoOutlinePlugin {
18    pub fn with_default_style(default_style: OutlineStyle) -> Self {
19        Self { default_style }
20    }
21}
22
23#[derive(Resource, Debug, Clone)]
24pub struct OutlineAssets {
25    default_material: Handle<StandardMaterial>,
26    default_style: OutlineStyle,
27}
28
29impl OutlineAssets {
30    pub fn default_material(&self) -> Handle<StandardMaterial> {
31        self.default_material.clone()
32    }
33
34    pub fn default_style(&self) -> OutlineStyle {
35        self.default_style
36    }
37}
38
39#[derive(Component, Debug, Clone, Copy)]
40pub struct OutlineShell;
41
42#[derive(Debug, Clone, Copy, PartialEq)]
43pub struct OutlineStyle {
44    pub color: Color,
45    pub emissive_strength: f32,
46    pub scale: Vec3,
47}
48
49impl Default for OutlineStyle {
50    fn default() -> Self {
51        Self {
52            color: Color::srgb(0.43, 0.94, 0.98),
53            emissive_strength: 2.0,
54            scale: Vec3::splat(1.08),
55        }
56    }
57}
58
59impl Plugin for GpmoOutlinePlugin {
60    fn build(&self, app: &mut App) {
61        let default_style = self.default_style;
62        app.insert_resource(PendingOutlineStyle(default_style))
63            .add_systems(Startup, setup_outline_assets);
64    }
65}
66
67#[derive(Resource, Debug, Clone, Copy)]
68struct PendingOutlineStyle(OutlineStyle);
69
70pub fn create_outline_material(
71    materials: &mut Assets<StandardMaterial>,
72    style: OutlineStyle,
73) -> Handle<StandardMaterial> {
74    materials.add(StandardMaterial {
75        base_color: style.color,
76        emissive: style.color.to_linear() * style.emissive_strength,
77        unlit: true,
78        cull_mode: Some(Face::Front),
79        ..default()
80    })
81}
82
83pub fn outline_shell_transform(style: OutlineStyle) -> Transform {
84    Transform::from_scale(style.scale)
85}
86
87pub fn spawn_outline_mesh<'a>(
88    parent: &'a mut ChildSpawnerCommands,
89    mesh: Handle<Mesh>,
90    material: Handle<StandardMaterial>,
91    style: OutlineStyle,
92    visibility: Visibility,
93) -> EntityCommands<'a> {
94    parent.spawn((
95        OutlineShell,
96        Mesh3d(mesh),
97        MeshMaterial3d(material),
98        outline_shell_transform(style),
99        visibility,
100    ))
101}
102
103pub fn spawn_default_outline_mesh<'a>(
104    parent: &'a mut ChildSpawnerCommands,
105    outline_assets: &OutlineAssets,
106    mesh: Handle<Mesh>,
107    visibility: Visibility,
108) -> EntityCommands<'a> {
109    spawn_outline_mesh(
110        parent,
111        mesh,
112        outline_assets.default_material(),
113        outline_assets.default_style(),
114        visibility,
115    )
116}
117
118fn setup_outline_assets(
119    mut commands: Commands,
120    pending_style: Res<PendingOutlineStyle>,
121    mut materials: ResMut<Assets<StandardMaterial>>,
122) {
123    let style = pending_style.0;
124    let default_material = create_outline_material(&mut materials, style);
125    commands.insert_resource(OutlineAssets {
126        default_material,
127        default_style: style,
128    });
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn default_style_has_positive_scale() {
137        let style = OutlineStyle::default();
138
139        assert!(style.scale.x > 1.0);
140        assert!(style.scale.y > 1.0);
141        assert!(style.scale.z > 1.0);
142    }
143
144    #[test]
145    fn outline_transform_uses_style_scale() {
146        let style = OutlineStyle {
147            scale: Vec3::splat(1.25),
148            ..default()
149        };
150
151        let transform = outline_shell_transform(style);
152
153        assert_eq!(transform.scale, Vec3::splat(1.25));
154    }
155}