Skip to main content

cloudiful_bevy_outline/
lib.rs

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