bevy_shape_draw/
draw.rs

1use bevy::prelude::{
2    debug, shape, warn, AlphaMode, Assets, Commands, Component, Entity, EventReader, EventWriter,
3    FromWorld, Handle, Local, Mesh, MouseButton, PbrBundle, Query, Res, ResMut, Resource,
4    StandardMaterial, TouchInput, Transform, Vec3, With, World,
5};
6use bevy_input::{touch::TouchPhase, Input};
7use bevy_mod_raycast::Intersection;
8
9use crate::ShapeDrawRaycastSet;
10
11#[derive(Resource)]
12pub struct BoxDrawResources {
13    pub material: Handle<StandardMaterial>,
14    /// The box created must have an initial size which is then changed
15    pub initial_size: f32,
16    /// The box will start with an initial height
17    pub initial_height: f32,
18}
19
20impl FromWorld for BoxDrawResources {
21    fn from_world(world: &mut World) -> Self {
22        let world = world.cell();
23        let mut materials = world
24            .get_resource_mut::<Assets<StandardMaterial>>()
25            .unwrap();
26
27        let material = materials.add(StandardMaterial {
28            base_color: bevy::prelude::Color::rgba(
29                0x10 as f32 / 0xFF as f32,
30                0x10 as f32 / 0xFF as f32,
31                0xF0 as f32 / 0xFF as f32,
32                0.5,
33            ),
34            alpha_mode: AlphaMode::Blend,
35            ..Default::default()
36        });
37
38        Self {
39            material,
40            initial_size: 0.01,
41            initial_height: 0.2,
42        }
43    }
44}
45
46pub(crate) fn keep_enabled(
47    mut event_writer: EventWriter<DrawStateEvent>,
48    state: Res<DrawingState>,
49) {
50    if let DrawingState::Disabled = *state {
51        event_writer.send(DrawStateEvent::Enable);
52    }
53}
54
55#[derive(Copy, Clone, Debug)]
56pub enum DrawShapeEvent {
57    /// Spawned is sent when a new shape is drawn, containing the newly created entity
58    Spawned(Entity),
59    Redrawing(Entity),
60    Finished(Entity),
61}
62
63pub enum DrawStateEvent {
64    Enable,
65    /// Enables Drawing if disabled and will use the provided entity to store the shape
66    Redraw(Entity),
67    Disable,
68}
69
70/// This component is added to everything drawn within this plugin.
71/// It contains the shape and the size of the object
72#[derive(Debug, Component)]
73pub enum Shape {
74    Box(Vec3),
75}
76
77#[derive(Resource)]
78pub(crate) enum DrawingState {
79    Idle(Option<Entity>),
80    Disabled,
81}
82
83impl Default for DrawingState {
84    fn default() -> Self {
85        Self::Disabled
86    }
87}
88
89#[derive(Component)]
90pub(crate) struct Editing(pub Vec3);
91
92pub(crate) fn draw_state(
93    mut event_reader: EventReader<DrawStateEvent>,
94    mut state: ResMut<DrawingState>,
95) {
96    for ev in event_reader.iter() {
97        match ev {
98            DrawStateEvent::Redraw(e) => *state = DrawingState::Idle(Some(*e)),
99            DrawStateEvent::Enable => *state = DrawingState::Idle(None),
100            DrawStateEvent::Disable => *state = DrawingState::Disabled,
101        }
102    }
103}
104
105#[derive(Resource, Default)]
106pub(crate) struct TouchId(Option<u64>);
107
108pub(crate) fn draw_box(
109    mut meshes: ResMut<Assets<Mesh>>,
110    query: Query<&Intersection<ShapeDrawRaycastSet>>,
111    keys: Res<Input<MouseButton>>,
112    resources: Res<BoxDrawResources>,
113    mut commands: Commands,
114    edit_box: Query<Entity, With<Editing>>,
115    shapes: Query<&Shape>,
116    mut event_writer: EventWriter<DrawShapeEvent>,
117    mut touch_events: EventReader<TouchInput>,
118    mut event_queue: Local<Vec<DrawShapeEvent>>,
119    state: Res<DrawingState>,
120    mut touch_id: ResMut<TouchId>,
121    mut touch_started: Local<bool>,
122) {
123    // We wait one frame before sending out the event to give time to spawn the entity
124    let mut next_event = event_queue.pop();
125    while next_event.is_some() {
126        event_writer.send(next_event.unwrap());
127        next_event = event_queue.pop();
128    }
129
130    let redraw = match *state {
131        DrawingState::Idle(e) => e,
132        _ => return,
133    };
134
135    let height = if let Some(e) = redraw {
136        let shape = shapes.get(e);
137        if let Ok(Shape::Box(dim)) = shape {
138            dim.y
139        } else {
140            resources.initial_height
141        }
142    } else {
143        resources.initial_height
144    };
145
146    let mut started = keys.just_pressed(MouseButton::Left);
147    let mut ended = keys.just_released(MouseButton::Left);
148
149    for ev in touch_events.iter() {
150        if let Some(id) = touch_id.0 {
151            if id != ev.id {
152                continue;
153            }
154        }
155
156        match ev.phase {
157            TouchPhase::Started => {
158                touch_id.0 = Some(ev.id);
159            }
160            TouchPhase::Ended | TouchPhase::Cancelled => {
161                if touch_id.0.is_some() {
162                    ended = true;
163                    *touch_started = false;
164                    touch_id.0 = None;
165                }
166            }
167            TouchPhase::Moved => {
168                started = !*touch_started;
169                *touch_started = true;
170            }
171        }
172    }
173    let intersect_position = get_closest_intersection(query);
174
175    if started {
176        // only do something if we actually have an intersection position
177        if let Some(intersect_position) = intersect_position {
178            let mut transform = Transform::default();
179            transform.translation = intersect_position
180                + Vec3::new(
181                    resources.initial_size / 2.,
182                    height / 2.,
183                    resources.initial_size / 2.,
184                );
185            let origin: Vec3 = intersect_position;
186
187            let mesh = meshes.add(Mesh::from(shape::Box::new(
188                resources.initial_size,
189                height,
190                resources.initial_size,
191            )));
192
193            let new_drawing = redraw.is_none();
194
195            let mut e_commands = match redraw {
196                Some(e) => commands.entity(e),
197                None => commands.spawn(PbrBundle {
198                    mesh: mesh.clone(),
199                    material: resources.material.clone(),
200                    transform,
201                    ..Default::default()
202                }),
203            };
204
205            let e = e_commands
206                .insert(Editing(origin))
207                .insert(Shape::Box(Vec3::new(
208                    resources.initial_size,
209                    height,
210                    resources.initial_size,
211                )))
212                .id();
213
214            if new_drawing {
215                event_queue.push(DrawShapeEvent::Spawned(e));
216            } else {
217                event_queue.push(DrawShapeEvent::Redrawing(e));
218            }
219        }
220    } else if ended {
221        if let Ok(e) = edit_box.get_single() {
222            commands.entity(e).remove::<Editing>();
223            event_queue.push(DrawShapeEvent::Finished(e));
224        }
225    }
226}
227
228fn get_closest_intersection<'a>(
229    query: Query<&'a Intersection<ShapeDrawRaycastSet>>,
230) -> Option<Vec3> {
231    let mut intersect_position = None;
232    // large value, we will only pick the closest pick-source in the case of multiple pick-sources
233    let mut distance = f32::INFINITY;
234    for intersection in &query {
235        debug!(
236            "Distance {:?}, Position {:?}",
237            intersection.distance(),
238            intersection.position()
239        );
240        //
241        if let (Some(dist), Some(pos)) = (intersection.distance(), intersection.position()) {
242            if dist < distance {
243                distance = dist;
244                intersect_position = Some(*pos);
245            }
246        }
247    }
248    intersect_position
249}
250
251pub(crate) fn edit_box(
252    mut e_box: Query<(&Handle<Mesh>, &mut Transform, &Editing, &mut Shape)>,
253    query: Query<&Intersection<ShapeDrawRaycastSet>>,
254    keys: Res<Input<MouseButton>>,
255    mut meshes: ResMut<Assets<Mesh>>,
256    state: Res<DrawingState>,
257    mut touch_events: EventReader<TouchInput>,
258    touch_id: Res<TouchId>,
259) {
260    match *state {
261        DrawingState::Disabled => return,
262        _ => {}
263    }
264
265    let mut update = keys.pressed(MouseButton::Left);
266
267    for ev in touch_events.iter() {
268        if let Some(id) = touch_id.0 {
269            if id != ev.id {
270                warn!("Wrong touch id");
271                continue;
272            }
273        }
274
275        update = true;
276        break;
277    }
278
279    if update {
280        if let Ok((handle, mut transform, edit_origin, mut shape)) = e_box.get_single_mut() {
281            if let Some(mesh) = meshes.get_mut(handle) {
282                let mut opposite = Vec3::default();
283
284                for intersection in &query {
285                    if let Some(pos) = intersection.position() {
286                        opposite = *pos;
287                    }
288                }
289
290                if opposite == Vec3::ZERO || opposite == edit_origin.0 {
291                    return;
292                }
293
294                let p1 = edit_origin.0;
295                let p2 = opposite;
296
297                let dx = p2.x - p1.x;
298                let dz = p2.z - p1.z;
299
300                let x = dx.abs();
301                let z = dz.abs();
302
303                let height = match &mut *shape {
304                    Shape::Box(size) => {
305                        size.x = x;
306                        size.z = z;
307                        size.y
308                    }
309                };
310
311                let b = shape::Box::new(x, height, z);
312
313                debug!("Box: {:?}", b);
314
315                *mesh = Mesh::from(b);
316                transform.translation.x = p2.x - (dx / 2.0);
317                transform.translation.z = p2.z - (dz / 2.0);
318                transform.translation.y = opposite.y + (height / 2.0);
319            }
320        } else {
321            /* TODO: There is currently a bug that when you are in the browser and are using the Device toolbar for touch.
322            If you spam enough boxes it will eventually fall into a state that only returns the warning below */
323            warn!("No editbox found");
324        }
325    }
326}