1use bevy::camera::RenderTarget;
9use bevy::camera::primitives::{Aabb, MeshAabb};
10use bevy::ecs::query::QueryFilter;
11use bevy::ecs::system::SystemParam;
12use bevy::mesh::VertexAttributeValues;
13use bevy::platform::collections::HashMap;
14use bevy::prelude::*;
15use bevy::render::render_resource::PrimitiveTopology;
16use bevy::window::{PrimaryWindow, WindowRef};
17use bevy_egui::{EguiContexts, egui};
18
19use crate::entity_management::YoleckRawEntry;
20use crate::knobs::YoleckKnobMarker;
21use crate::prelude::{YoleckEditorState, YoleckUi};
22use crate::{
23 YoleckDirective, YoleckEditMarker, YoleckEditorEvent, YoleckEditorViewportRect,
24 YoleckEntityConstructionSpecs, YoleckManaged, YoleckRunEditSystems, YoleckState,
25};
26
27pub mod prelude {
28 pub use crate::vpeol::{
29 VpeolCameraState, VpeolDragPlane, VpeolOverrideDragPlane, VpeolRepositionLevel,
30 VpeolSelectionCuePlugin, VpeolWillContainClickableChildren, YoleckKnobClick,
31 };
32 #[cfg(feature = "vpeol_2d")]
33 pub use crate::vpeol_2d::{
34 Vpeol2dCameraControl, Vpeol2dPluginForEditor, Vpeol2dPluginForGame, Vpeol2dPosition,
35 Vpeol2dRotatation, Vpeol2dScale,
36 };
37 #[cfg(feature = "vpeol_3d")]
38 pub use crate::vpeol_3d::{
39 Vpeol3dCameraControl, Vpeol3dCameraMode, Vpeol3dPluginForEditor, Vpeol3dPluginForGame,
40 Vpeol3dPosition, Vpeol3dRotation, Vpeol3dScale, Vpeol3dSnapToPlane,
41 Vpeol3dTranslationGizmoConfig, Vpeol3dTranslationGizmoMode, YoleckCameraChoices,
42 };
43}
44
45#[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)]
47pub enum VpeolSystems {
48 PrepareCameraState,
54 UpdateCameraState,
57 HandleCameraState,
59}
60
61pub struct VpeolBasePlugin;
63
64impl Plugin for VpeolBasePlugin {
65 fn build(&self, app: &mut App) {
66 app.configure_sets(
67 Update,
68 (
69 VpeolSystems::PrepareCameraState.run_if(in_state(YoleckEditorState::EditorActive)),
70 VpeolSystems::UpdateCameraState.run_if(in_state(YoleckEditorState::EditorActive)),
71 VpeolSystems::HandleCameraState.run_if(in_state(YoleckEditorState::EditorActive)),
72 YoleckRunEditSystems,
73 )
74 .chain(), );
76 app.init_resource::<VpeolClipboard>();
77 app.add_systems(
78 Update,
79 (prepare_camera_state, update_camera_world_position)
80 .in_set(VpeolSystems::PrepareCameraState),
81 );
82 app.add_systems(
83 Update,
84 handle_camera_state.in_set(VpeolSystems::HandleCameraState),
85 );
86 app.add_systems(
87 Update,
88 (
89 handle_delete_entity_key,
90 handle_copy_entity_key,
91 handle_paste_entity_key,
92 )
93 .run_if(in_state(YoleckEditorState::EditorActive)),
94 );
95 #[cfg(feature = "bevy_reflect")]
96 app.register_type::<VpeolDragPlane>()
97 .register_type::<VpeolOverrideDragPlane>();
98 }
99}
100
101#[derive(Resource)]
114#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
115pub struct VpeolDragPlane(pub InfinitePlane3d);
116
117impl VpeolDragPlane {
118 pub const XY: Self = Self(InfinitePlane3d { normal: Dir3::Z });
119 pub const XZ: Self = Self(InfinitePlane3d { normal: Dir3::Y });
120}
121
122#[derive(Component)]
129#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
130pub struct VpeolOverrideDragPlane(pub InfinitePlane3d);
131
132impl VpeolOverrideDragPlane {
133 pub const XY: Self = Self(InfinitePlane3d { normal: Dir3::Z });
134 pub const XZ: Self = Self(InfinitePlane3d { normal: Dir3::Y });
135}
136
137#[derive(Component, Default, Debug)]
139pub struct VpeolCameraState {
140 pub cursor_ray: Option<Ray3d>,
142 pub entity_under_cursor: Option<(Entity, VpeolCursorPointing)>,
144 pub entities_of_interest: HashMap<Entity, Option<VpeolCursorPointing>>,
147 pub clicks_on_objects_state: VpeolClicksOnObjectsState,
149}
150
151impl FromTemplate for VpeolCameraState {
152 type Template = VpeolCameraStateTemplate;
153}
154
155#[derive(Default)]
156pub struct VpeolCameraStateTemplate;
157
158impl Template for VpeolCameraStateTemplate {
159 type Output = VpeolCameraState;
160
161 fn build_template(&self, _: &mut bevy::ecs::template::TemplateContext) -> Result<Self::Output> {
162 Ok(VpeolCameraState::default())
163 }
164
165 fn clone_template(&self) -> Self {
166 Self
167 }
168}
169
170#[derive(Clone, Debug)]
172pub struct VpeolCursorPointing {
173 pub cursor_position_world_coords: Vec3,
175 pub z_depth_screen_coords: f32,
177}
178
179#[derive(Default, Debug)]
181pub enum VpeolClicksOnObjectsState {
182 #[default]
183 Empty,
184 BeingDragged {
185 entity: Entity,
186 prev_screen_pos: Vec2,
188 offset: Vec3,
190 select_on_mouse_release: bool,
191 },
192}
193
194impl VpeolCameraState {
195 pub fn consider(
200 &mut self,
201 entity: Entity,
202 z_depth_screen_coords: f32,
203 cursor_position_world_coords: impl FnOnce() -> Vec3,
204 ) {
205 let should_update_entity = if let Some((_, old_cursor)) = self.entity_under_cursor.as_ref()
206 {
207 old_cursor.z_depth_screen_coords < z_depth_screen_coords
208 } else {
209 true
210 };
211
212 if let Some(of_interest) = self.entities_of_interest.get_mut(&entity) {
213 let pointing = VpeolCursorPointing {
214 cursor_position_world_coords: cursor_position_world_coords(),
215 z_depth_screen_coords,
216 };
217 if should_update_entity {
218 self.entity_under_cursor = Some((entity, pointing.clone()));
219 }
220 *of_interest = Some(pointing);
221 } else if should_update_entity {
222 self.entity_under_cursor = Some((
223 entity,
224 VpeolCursorPointing {
225 cursor_position_world_coords: cursor_position_world_coords(),
226 z_depth_screen_coords,
227 },
228 ));
229 }
230 }
231
232 pub fn pointing_at_entity(&self, entity: Entity) -> Option<&VpeolCursorPointing> {
233 if let Some((entity_under_cursor, pointing_at)) = &self.entity_under_cursor
234 && *entity_under_cursor == entity
235 {
236 return Some(pointing_at);
237 }
238 self.entities_of_interest.get(&entity)?.as_ref()
239 }
240}
241
242fn prepare_camera_state(
243 mut query: Query<&mut VpeolCameraState>,
244 knob_query: Query<Entity, With<YoleckKnobMarker>>,
245) {
246 for mut camera_state in query.iter_mut() {
247 camera_state.entity_under_cursor = None;
248 camera_state.entities_of_interest = knob_query
249 .iter()
250 .chain(match camera_state.clicks_on_objects_state {
251 VpeolClicksOnObjectsState::Empty => None,
252 VpeolClicksOnObjectsState::BeingDragged { entity, .. } => Some(entity),
253 })
254 .map(|entity| (entity, None))
255 .collect();
256 }
257}
258
259fn update_camera_world_position(
260 mut cameras_query: Query<(
261 &mut VpeolCameraState,
262 &GlobalTransform,
263 &Camera,
264 &RenderTarget,
265 )>,
266 window_getter: WindowGetter,
267) {
268 for (mut camera_state, camera_transform, camera, render_target) in cameras_query.iter_mut() {
269 camera_state.cursor_ray = (|| {
270 let RenderTarget::Window(window_ref) = render_target else {
271 return None;
272 };
273 let window = window_getter.get_window(*window_ref)?;
274 let cursor_in_screen_pos = window.cursor_position()?;
275 camera
276 .viewport_to_world(camera_transform, cursor_in_screen_pos)
277 .ok()
278 })();
279 }
280}
281
282#[derive(SystemParam)]
283pub(crate) struct WindowGetter<'w, 's> {
284 windows: Query<'w, 's, &'static Window>,
285 primary_window: Query<'w, 's, &'static Window, With<PrimaryWindow>>,
286}
287
288impl WindowGetter<'_, '_> {
289 pub fn get_window(&self, window_ref: WindowRef) -> Option<&Window> {
290 match window_ref {
291 WindowRef::Primary => self.primary_window.single().ok(),
292 WindowRef::Entity(window_id) => self.windows.get(window_id).ok(),
293 }
294 }
295}
296
297#[allow(clippy::too_many_arguments)]
298fn handle_camera_state(
299 mut egui_context: EguiContexts,
300 mut query: Query<(&RenderTarget, &mut VpeolCameraState)>,
301 window_getter: WindowGetter,
302 mouse_buttons: Res<ButtonInput<MouseButton>>,
303 keyboard: Res<ButtonInput<KeyCode>>,
304 global_transform_query: Query<&GlobalTransform>,
305 selected_query: Query<(), With<YoleckEditMarker>>,
306 knob_query: Query<Entity, With<YoleckKnobMarker>>,
307 mut directives_writer: MessageWriter<YoleckDirective>,
308 global_drag_plane: Res<VpeolDragPlane>,
309 drag_plane_overrides_query: Query<&VpeolOverrideDragPlane>,
310 editor_viewport: Res<YoleckEditorViewportRect>,
311) -> Result {
312 enum MouseButtonOp {
313 JustPressed,
314 BeingPressed,
315 JustReleased,
316 }
317 let mouse_button_op = if mouse_buttons.just_pressed(MouseButton::Left) {
318 if egui_context.ctx_mut()?.is_pointer_over_egui() {
319 return Ok(());
320 }
321 MouseButtonOp::JustPressed
322 } else if mouse_buttons.just_released(MouseButton::Left) {
323 MouseButtonOp::JustReleased
324 } else if mouse_buttons.pressed(MouseButton::Left) {
325 MouseButtonOp::BeingPressed
326 } else {
327 for (_, mut camera_state) in query.iter_mut() {
328 camera_state.clicks_on_objects_state = VpeolClicksOnObjectsState::Empty;
329 }
330 return Ok(());
331 };
332 for (render_target, mut camera_state) in query.iter_mut() {
333 let Some(cursor_ray) = camera_state.cursor_ray else {
334 continue;
335 };
336 let calc_cursor_in_world_position = |entity: Entity, plane_origin: Vec3| -> Option<Vec3> {
337 let drag_plane = if let Ok(VpeolOverrideDragPlane(drag_plane)) =
338 drag_plane_overrides_query.get(entity)
339 {
340 drag_plane
341 } else {
342 &global_drag_plane.0
343 };
344 let distance = cursor_ray.intersect_plane(plane_origin, *drag_plane)?;
345 Some(cursor_ray.get_point(distance))
346 };
347
348 let RenderTarget::Window(window_ref) = render_target else {
349 continue;
350 };
351 let Some(window) = window_getter.get_window(*window_ref) else {
352 continue;
353 };
354 let Some(cursor_in_screen_pos) = window.cursor_position() else {
355 continue;
356 };
357 if matches!(*window_ref, WindowRef::Primary)
358 && matches!(mouse_button_op, MouseButtonOp::JustPressed)
359 && editor_viewport.rect.is_some_and(|rect| {
360 !rect.contains(egui::Pos2::new(
361 cursor_in_screen_pos.x,
362 cursor_in_screen_pos.y,
363 ))
364 })
365 {
366 continue;
367 }
368
369 match (&mouse_button_op, &camera_state.clicks_on_objects_state) {
370 (MouseButtonOp::JustPressed, VpeolClicksOnObjectsState::Empty) => {
371 if keyboard.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]) {
372 if let Some((entity, _)) = &camera_state.entity_under_cursor {
373 directives_writer.write(YoleckDirective::toggle_selected(*entity));
374 }
375 } else if let Some((knob_entity, cursor_pointing)) =
376 knob_query.iter().find_map(|knob_entity| {
377 Some((knob_entity, camera_state.pointing_at_entity(knob_entity)?))
378 })
379 {
380 directives_writer.write(YoleckDirective::pass_to_entity(
381 knob_entity,
382 YoleckKnobClick,
383 ));
384 let Ok(knob_transform) = global_transform_query.get(knob_entity) else {
385 continue;
386 };
387 let Some(cursor_in_world_position) = calc_cursor_in_world_position(
388 knob_entity,
389 cursor_pointing.cursor_position_world_coords,
390 ) else {
391 continue;
392 };
393 camera_state.clicks_on_objects_state = VpeolClicksOnObjectsState::BeingDragged {
394 entity: knob_entity,
395 prev_screen_pos: cursor_in_screen_pos,
396 offset: cursor_in_world_position - knob_transform.translation(),
397 select_on_mouse_release: false,
398 }
399 } else {
400 camera_state.clicks_on_objects_state = if let Some((entity, cursor_pointing)) =
401 &camera_state.entity_under_cursor
402 {
403 let Ok(entity_transform) = global_transform_query.get(*entity) else {
404 continue;
405 };
406 let select_on_mouse_release = selected_query.contains(*entity);
407 if !select_on_mouse_release {
408 directives_writer.write(YoleckDirective::set_selected(Some(*entity)));
409 }
410 let Some(cursor_in_world_position) = calc_cursor_in_world_position(
411 *entity,
412 cursor_pointing.cursor_position_world_coords,
413 ) else {
414 continue;
415 };
416 VpeolClicksOnObjectsState::BeingDragged {
417 entity: *entity,
418 prev_screen_pos: cursor_in_screen_pos,
419 offset: cursor_in_world_position - entity_transform.translation(),
420 select_on_mouse_release,
421 }
422 } else {
423 directives_writer.write(YoleckDirective::set_selected(None));
424 VpeolClicksOnObjectsState::Empty
425 };
426 }
427 }
428 (
429 MouseButtonOp::BeingPressed,
430 VpeolClicksOnObjectsState::BeingDragged {
431 entity,
432 prev_screen_pos,
433 offset,
434 select_on_mouse_release: _,
435 },
436 ) => {
437 if 0.1 <= prev_screen_pos.distance_squared(cursor_in_screen_pos) {
438 let Ok(entity_transform) = global_transform_query.get(*entity) else {
439 continue;
440 };
441 let drag_point = entity_transform.translation() + *offset;
442 let Some(cursor_in_world_position) =
443 calc_cursor_in_world_position(*entity, drag_point)
444 else {
445 continue;
446 };
447 directives_writer.write(YoleckDirective::pass_to_entity(
448 *entity,
449 cursor_in_world_position - *offset,
450 ));
451 camera_state.clicks_on_objects_state =
452 VpeolClicksOnObjectsState::BeingDragged {
453 entity: *entity,
454 prev_screen_pos: cursor_in_screen_pos,
455 offset: *offset,
456 select_on_mouse_release: false,
457 };
458 }
459 }
460 (
461 MouseButtonOp::JustReleased,
462 VpeolClicksOnObjectsState::BeingDragged {
463 entity,
464 prev_screen_pos: _,
465 offset: _,
466 select_on_mouse_release: true,
467 },
468 ) => {
469 directives_writer.write(YoleckDirective::set_selected(Some(*entity)));
470 camera_state.clicks_on_objects_state = VpeolClicksOnObjectsState::Empty;
471 }
472 _ => {}
473 }
474 }
475 Ok(())
476}
477
478pub struct YoleckKnobClick;
481
482#[derive(Component, Default, Clone)]
487pub struct VpeolWillContainClickableChildren;
488
489#[derive(Component)]
491pub struct VpeolRouteClickTo(pub Entity);
492
493#[derive(SystemParam)]
496pub struct VpeolRootResolver<'w, 's> {
497 root_resolver: Query<'w, 's, &'static VpeolRouteClickTo>,
498 #[allow(clippy::type_complexity)]
499 has_managed_query: Query<'w, 's, (), Or<(With<YoleckManaged>, With<YoleckKnobMarker>)>>,
500}
501
502impl VpeolRootResolver<'_, '_> {
503 pub fn resolve_root(&self, entity: Entity) -> Option<Entity> {
505 if let Ok(VpeolRouteClickTo(root_entity)) = self.root_resolver.get(entity) {
506 Some(*root_entity)
507 } else {
508 self.has_managed_query.get(entity).ok()?;
509 Some(entity)
510 }
511 }
512}
513
514pub fn handle_clickable_children_system<F, B>(
516 parents_query: Query<(Entity, &Children), With<VpeolWillContainClickableChildren>>,
517 children_query: Query<&Children>,
518 should_add_query: Query<Entity, F>,
519 mut commands: Commands,
520) where
521 F: QueryFilter,
522 B: Default + Bundle,
523{
524 for (parent, children) in parents_query.iter() {
525 if children.is_empty() {
526 continue;
527 }
528 let mut any_added = false;
529 let mut children_to_check: Vec<Entity> = children.iter().collect();
530 while let Some(child) = children_to_check.pop() {
531 if let Ok(child_children) = children_query.get(child) {
532 children_to_check.extend(child_children.iter());
533 }
534 if should_add_query.get(child).is_ok() {
535 commands
536 .entity(child)
537 .try_insert((VpeolRouteClickTo(parent), B::default()));
538 any_added = true;
539 }
540 }
541 if any_added {
542 commands
543 .entity(parent)
544 .remove::<VpeolWillContainClickableChildren>();
545 }
546 }
547}
548
549pub struct VpeolSelectionCuePlugin {
551 pub effect_duration: f32,
553 pub effect_magnitude: f32,
555}
556
557impl Default for VpeolSelectionCuePlugin {
558 fn default() -> Self {
559 Self {
560 effect_duration: 0.3,
561 effect_magnitude: 0.3,
562 }
563 }
564}
565
566impl Plugin for VpeolSelectionCuePlugin {
567 fn build(&self, app: &mut App) {
568 app.add_systems(Update, manage_selection_transform_components);
569 app.add_systems(PostUpdate, {
570 add_selection_cue_before_transform_propagate(
571 1.0 / self.effect_duration,
572 2.0 * self.effect_magnitude,
573 )
574 .before(TransformSystems::Propagate)
575 });
576 app.add_systems(PostUpdate, {
577 restore_transform_from_cache_after_transform_propagate
578 .after(TransformSystems::Propagate)
579 });
580 }
581}
582
583#[derive(Component)]
584struct SelectionCueAnimation {
585 cached_transform: Transform,
586 progress: f32,
587}
588
589fn manage_selection_transform_components(
590 add_cue_query: Query<Entity, (Without<SelectionCueAnimation>, With<YoleckEditMarker>)>,
591 remove_cue_query: Query<Entity, (With<SelectionCueAnimation>, Without<YoleckEditMarker>)>,
592 mut commands: Commands,
593) {
594 for entity in add_cue_query.iter() {
595 commands.entity(entity).insert(SelectionCueAnimation {
596 cached_transform: Default::default(),
597 progress: 0.0,
598 });
599 }
600 for entity in remove_cue_query.iter() {
601 commands.entity(entity).remove::<SelectionCueAnimation>();
602 }
603}
604
605fn add_selection_cue_before_transform_propagate(
606 time_speedup: f32,
607 magnitude_scale: f32,
608) -> impl FnMut(Query<(&mut SelectionCueAnimation, &mut Transform)>, Res<Time>) {
609 move |mut query, time| {
610 for (mut animation, mut transform) in query.iter_mut() {
611 animation.cached_transform = *transform;
612 if animation.progress < 1.0 {
613 animation.progress += time_speedup * time.delta_secs();
614 let extra = if animation.progress < 0.5 {
615 animation.progress
616 } else {
617 1.0 - animation.progress
618 };
619 transform.scale *= 1.0 + magnitude_scale * extra;
620 }
621 }
622 }
623}
624
625fn restore_transform_from_cache_after_transform_propagate(
626 mut query: Query<(&SelectionCueAnimation, &mut Transform)>,
627) {
628 for (animation, mut transform) in query.iter_mut() {
629 *transform = animation.cached_transform;
630 }
631}
632
633pub(crate) fn ray_intersection_with_mesh(ray: Ray3d, mesh: &Mesh) -> Option<f32> {
634 let aabb = mesh.get_aabb()?;
635 let distance_to_aabb = ray_intersection_with_aabb(ray, aabb)?;
636
637 if let Some(mut triangles) = iter_triangles(mesh) {
638 triangles.find_map(|triangle| triangle.ray_intersection(ray))
639 } else {
640 Some(distance_to_aabb)
641 }
642}
643
644fn ray_intersection_with_aabb(ray: Ray3d, aabb: Aabb) -> Option<f32> {
645 let center: Vec3 = aabb.center.into();
646 let mut max_low = f32::NEG_INFINITY;
647 let mut min_high = f32::INFINITY;
648 for (axis, half_extent) in [
649 (Vec3::X, aabb.half_extents.x),
650 (Vec3::Y, aabb.half_extents.y),
651 (Vec3::Z, aabb.half_extents.z),
652 ] {
653 let dot = ray.direction.dot(axis);
654 if dot == 0.0 {
655 let distance_from_center = (ray.origin - center).dot(axis);
656 if half_extent < distance_from_center.abs() {
657 return None;
658 }
659 } else {
660 let low = ray.intersect_plane(center - half_extent * axis, InfinitePlane3d::new(axis));
661 let high = ray.intersect_plane(center + half_extent * axis, InfinitePlane3d::new(axis));
662 let (low, high) = if 0.0 <= dot { (low, high) } else { (high, low) };
663 if let Some(low) = low {
664 max_low = max_low.max(low);
665 }
666 min_high = min_high.min(high?);
667 }
668 }
669 if max_low <= min_high {
670 Some(max_low)
671 } else {
672 None
673 }
674}
675
676fn iter_triangles(mesh: &Mesh) -> Option<impl '_ + Iterator<Item = Triangle>> {
677 if mesh.primitive_topology() != PrimitiveTopology::TriangleList {
678 return None;
679 }
680 let indices = mesh.indices()?;
681 let Some(VertexAttributeValues::Float32x3(positions)) =
682 mesh.attribute(Mesh::ATTRIBUTE_POSITION)
683 else {
684 return None;
685 };
686 let mut it = indices.iter();
687 Some(std::iter::from_fn(move || {
688 Some(Triangle(
689 [it.next()?, it.next()?, it.next()?].map(|idx| Vec3::from_array(positions[idx])),
690 ))
691 }))
692}
693
694#[derive(Debug)]
695struct Triangle([Vec3; 3]);
696
697impl Triangle {
698 fn ray_intersection(&self, ray: Ray3d) -> Option<f32> {
699 let directions = [
700 self.0[1] - self.0[0],
701 self.0[2] - self.0[1],
702 self.0[0] - self.0[2],
703 ];
704 let normal = directions[0].cross(directions[1]); let plane = InfinitePlane3d {
706 normal: Dir3::new(normal).ok()?,
707 };
708 let distance = ray.intersect_plane(self.0[0], plane)?;
709 let point = ray.get_point(distance);
710 if self
711 .0
712 .iter()
713 .zip(directions.iter())
714 .all(|(vertex, direction)| {
715 let vertical = direction.cross(normal);
716 vertical.dot(point - *vertex) <= 0.0
717 })
718 {
719 Some(distance)
720 } else {
721 None
722 }
723 }
724}
725
726pub fn vpeol_read_click_on_entity<Filter: QueryFilter>(
733 mut ui: ResMut<YoleckUi>,
734 cameras_query: Query<&VpeolCameraState>,
735 yoleck_managed_query: Query<&YoleckManaged>,
736 filter_query: Query<(), Filter>,
737 buttons: Res<ButtonInput<MouseButton>>,
738 mut candidate: Local<Option<Entity>>,
739) -> Option<Entity> {
740 let target = if ui.ctx().is_pointer_over_egui() {
741 None
742 } else {
743 cameras_query
744 .iter()
745 .find_map(|camera_state| Some(camera_state.entity_under_cursor.as_ref()?.0))
746 };
747
748 let Some(target) = target else {
749 ui.label("No Target");
750 return None;
751 };
752
753 let Ok(yoleck_managed) = yoleck_managed_query.get(target) else {
754 ui.label("No Target");
755 return None;
756 };
757
758 if !filter_query.contains(target) {
759 ui.label(format!("Invalid Target ({})", yoleck_managed.type_name));
760 return None;
761 }
762 ui.label(format!(
763 "Targeting {:?} ({})",
764 target, yoleck_managed.type_name
765 ));
766
767 if buttons.just_pressed(MouseButton::Left) {
768 *candidate = Some(target);
769 } else if buttons.just_released(MouseButton::Left)
770 && let Some(candidate) = candidate.take()
771 && candidate == target
772 {
773 return Some(target);
774 }
775 None
776}
777
778#[derive(Component, Default, Clone)]
789pub struct VpeolRepositionLevel(pub Transform);
790
791fn handle_delete_entity_key(
792 mut egui_context: EguiContexts,
793 keyboard_input: Res<ButtonInput<KeyCode>>,
794 mut yoleck_state: ResMut<YoleckState>,
795 query: Query<Entity, With<YoleckEditMarker>>,
796 mut commands: Commands,
797 mut writer: MessageWriter<YoleckEditorEvent>,
798) -> Result {
799 if egui_context.ctx_mut()?.egui_wants_keyboard_input() {
800 return Ok(());
801 }
802
803 if keyboard_input.just_pressed(KeyCode::Delete) {
804 for entity in query.iter() {
805 commands.entity(entity).despawn();
806 writer.write(YoleckEditorEvent::EntityDeselected(entity));
807 }
808 if !query.is_empty() {
809 yoleck_state.level_needs_saving = true;
810 }
811 }
812
813 Ok(())
814}
815
816#[derive(Resource)]
817enum VpeolClipboard {
818 #[cfg(feature = "arboard")]
819 Arboard(arboard::Clipboard),
820 Internal(String),
821}
822
823impl FromWorld for VpeolClipboard {
824 fn from_world(_: &mut World) -> Self {
825 #[cfg(feature = "arboard")]
826 match arboard::Clipboard::new() {
827 Ok(clipboard) => {
828 debug!("Arboard clipbaord successfully initiated");
829 return VpeolClipboard::Arboard(clipboard);
830 }
831 Err(err) => {
832 warn!("Cannot initiate Arboard clipboard: {err}");
833 }
834 }
835 VpeolClipboard::Internal(String::new())
836 }
837}
838
839fn handle_copy_entity_key(
840 mut egui_context: EguiContexts,
841 keyboard_input: Res<ButtonInput<KeyCode>>,
842 query: Query<&YoleckManaged, With<YoleckEditMarker>>,
843 construction_specs: Res<YoleckEntityConstructionSpecs>,
844 mut clipboard: ResMut<VpeolClipboard>,
845) -> Result {
846 if egui_context.ctx_mut()?.egui_wants_keyboard_input() {
847 return Ok(());
848 }
849
850 let ctrl_pressed = keyboard_input.any_pressed([KeyCode::ControlLeft, KeyCode::ControlRight]);
851
852 if ctrl_pressed && keyboard_input.just_pressed(KeyCode::KeyC) {
853 let entities: Vec<YoleckRawEntry> = query
854 .iter()
855 .filter_map(|yoleck_managed| {
856 let entity_type =
857 construction_specs.get_entity_type_info(&yoleck_managed.type_name)?;
858
859 let data: serde_json::Map<String, serde_json::Value> = entity_type
860 .components
861 .iter()
862 .filter_map(|component| {
863 let component_data = yoleck_managed.components_data.get(component)?;
864 let handler = &construction_specs.component_handlers[component];
865 Some((
866 handler.key().to_string(),
867 handler.serialize(component_data.as_ref()),
868 ))
869 })
870 .collect();
871
872 Some(YoleckRawEntry {
873 header: crate::entity_management::YoleckEntryHeader {
874 type_name: yoleck_managed.type_name.clone(),
875 name: yoleck_managed.name.clone(),
876 uuid: None,
877 },
878 data,
879 })
880 })
881 .collect();
882
883 if !entities.is_empty()
884 && let Ok(json) = serde_json::to_string(&entities)
885 {
886 match clipboard.as_mut() {
887 #[cfg(feature = "arboard")]
888 VpeolClipboard::Arboard(clipboard) => {
889 clipboard.set_text(json)?;
890 }
891 VpeolClipboard::Internal(clipboard) => {
892 *clipboard = json;
893 }
894 }
895 }
896 }
897
898 Ok(())
899}
900
901fn handle_paste_entity_key(
902 mut egui_context: EguiContexts,
903 keyboard_input: Res<ButtonInput<KeyCode>>,
904 yoleck_state: Res<YoleckState>,
905 mut directives_writer: MessageWriter<YoleckDirective>,
906 mut clipboard: ResMut<VpeolClipboard>,
907) -> Result {
908 if egui_context.ctx_mut()?.egui_wants_keyboard_input() {
909 return Ok(());
910 }
911
912 let ctrl_pressed = keyboard_input.pressed(KeyCode::ControlLeft)
913 || keyboard_input.pressed(KeyCode::ControlRight);
914
915 if ctrl_pressed && keyboard_input.just_pressed(KeyCode::KeyV) {
916 #[cfg(feature = "arboard")]
917 let arboard_text_storage: String;
918 let text_to_paste: Option<&str> = match clipboard.as_mut() {
919 #[cfg(feature = "arboard")]
920 VpeolClipboard::Arboard(clipboard) => match clipboard.get_text() {
921 Ok(text) => {
922 arboard_text_storage = text;
923 Some(&arboard_text_storage)
924 }
925 Err(err) => {
926 error!("Cannot load text from arboard: {err}");
927 None
928 }
929 },
930 VpeolClipboard::Internal(clipboard) => {
931 Some(clipboard.as_str()).filter(|txt| !txt.is_empty())
932 }
933 };
934
935 if let Some(text) = text_to_paste
936 && let Ok(entities) =
937 serde_json::from_str::<Vec<YoleckRawEntry>>(text).inspect_err(|err| {
938 warn!("Cannot paste - failure to parse copied text: {err}");
939 })
940 && !entities.is_empty()
941 {
942 let level_being_edited = yoleck_state.level_being_edited;
943
944 for entry in entities {
945 directives_writer.write(
946 YoleckDirective::spawn_entity(level_being_edited, entry.header.type_name, true)
947 .extend(entry.data.into_iter())
948 .into(),
949 );
950 }
951 }
952 }
953
954 Ok(())
955}