cloudiful_bevy_outline/
lib.rs1use 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}