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