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