1use std::any::TypeId;
87
88use crate::bevy_egui::{EguiContexts, egui};
89use crate::exclusive_systems::{
90 YoleckEntityCreationExclusiveSystems, YoleckExclusiveSystemDirective,
91};
92use crate::vpeol::{
93 VpeolBasePlugin, VpeolCameraState, VpeolDragPlane, VpeolRepositionLevel, VpeolRootResolver,
94 VpeolSystems, WindowGetter, handle_clickable_children_system, ray_intersection_with_mesh,
95};
96use bevy::camera::RenderTarget;
97use bevy::camera::visibility::VisibleEntities;
98use bevy::input::mouse::MouseWheel;
99use bevy::math::DVec2;
100use bevy::platform::collections::HashMap;
101use bevy::prelude::*;
102use bevy::sprite::Anchor;
103use bevy::text::TextLayoutInfo;
104use serde::{Deserialize, Serialize};
105
106use crate::{YoleckBelongsToLevel, YoleckSchedule, prelude::*};
107
108pub struct Vpeol2dPluginForGame;
110
111impl Plugin for Vpeol2dPluginForGame {
112 fn build(&self, app: &mut App) {
113 app.add_systems(
114 YoleckSchedule::OverrideCommonComponents,
115 vpeol_2d_populate_transform,
116 );
117 #[cfg(feature = "bevy_reflect")]
118 register_reflect_types(app);
119 }
120}
121
122#[cfg(feature = "bevy_reflect")]
123fn register_reflect_types(app: &mut App) {
124 app.register_type::<Vpeol2dPosition>();
125 app.register_type::<Vpeol2dRotatation>();
126 app.register_type::<Vpeol2dScale>();
127 app.register_type::<Vpeol2dCameraControl>();
128}
129
130pub struct Vpeol2dPluginForEditor;
137
138impl Plugin for Vpeol2dPluginForEditor {
139 fn build(&self, app: &mut App) {
140 app.add_plugins(VpeolBasePlugin);
141 app.add_plugins(Vpeol2dPluginForGame);
142 app.insert_resource(VpeolDragPlane::XY);
143
144 app.add_systems(
145 Update,
146 (
147 update_camera_status_for_sprites,
148 update_camera_status_for_2d_meshes,
149 update_camera_status_for_text_2d,
150 )
151 .in_set(VpeolSystems::UpdateCameraState),
152 );
153 app.add_systems(
154 PostUpdate, (camera_2d_pan, camera_2d_zoom).run_if(in_state(YoleckEditorState::EditorActive)),
156 );
157 app.add_systems(
158 Update,
159 (
160 ApplyDeferred,
161 handle_clickable_children_system::<
162 Or<(With<Sprite>, (With<TextLayoutInfo>, With<Anchor>))>,
163 (),
164 >,
165 ApplyDeferred,
166 )
167 .chain()
168 .run_if(in_state(YoleckEditorState::EditorActive)),
169 );
170 app.add_yoleck_edit_system(vpeol_2d_edit_transform_group);
171 app.world_mut()
172 .resource_mut::<YoleckEntityCreationExclusiveSystems>()
173 .on_entity_creation(|queue| queue.push_back(vpeol_2d_init_position));
174 }
175}
176
177struct CursorInWorldPos {
178 cursor_in_world_pos: Vec2,
179}
180
181impl CursorInWorldPos {
182 fn from_camera_state(camera_state: &VpeolCameraState) -> Option<Self> {
183 Some(Self {
184 cursor_in_world_pos: camera_state.cursor_ray?.origin.truncate(),
185 })
186 }
187
188 fn cursor_in_entity_space(&self, transform: &GlobalTransform) -> Vec2 {
189 transform
190 .to_matrix()
191 .inverse()
192 .project_point3(self.cursor_in_world_pos.extend(0.0))
193 .truncate()
194 }
195
196 fn check_square(
197 &self,
198 entity_transform: &GlobalTransform,
199 anchor: &Anchor,
200 size: Vec2,
201 ) -> bool {
202 let cursor = self.cursor_in_entity_space(entity_transform);
203 let anchor = anchor.as_vec();
204 let mut min_corner = Vec2::new(-0.5, -0.5) - anchor;
205 let mut max_corner = Vec2::new(0.5, 0.5) - anchor;
206 for corner in [&mut min_corner, &mut max_corner] {
207 corner.x *= size.x;
208 corner.y *= size.y;
209 }
210 min_corner.x <= cursor.x
211 && cursor.x <= max_corner.x
212 && min_corner.y <= cursor.y
213 && cursor.y <= max_corner.y
214 }
215}
216
217#[allow(clippy::type_complexity)]
218fn update_camera_status_for_sprites(
219 mut cameras_query: Query<(&mut VpeolCameraState, &VisibleEntities)>,
220 entities_query: Query<(Entity, &GlobalTransform, &Sprite, &Anchor)>,
221 image_assets: Res<Assets<Image>>,
222 texture_atlas_layout_assets: Res<Assets<TextureAtlasLayout>>,
223 root_resolver: VpeolRootResolver,
224) {
225 for (mut camera_state, visible_entities) in cameras_query.iter_mut() {
226 let Some(cursor) = CursorInWorldPos::from_camera_state(&camera_state) else {
227 continue;
228 };
229
230 for (entity, entity_transform, sprite, anchor) in
231 entities_query.iter_many(visible_entities.iter(TypeId::of::<Sprite>()))
232 {
234 let size = if let Some(custom_size) = sprite.custom_size {
235 custom_size
236 } else if let Some(texture_atlas) = sprite.texture_atlas.as_ref() {
237 let Some(texture_atlas_layout) =
238 texture_atlas_layout_assets.get(&texture_atlas.layout)
239 else {
240 continue;
241 };
242 texture_atlas_layout.textures[texture_atlas.index]
243 .size()
244 .as_vec2()
245 } else if let Some(texture) = image_assets.get(&sprite.image) {
246 texture.size().as_vec2()
247 } else {
248 continue;
249 };
250 if cursor.check_square(entity_transform, anchor, size) {
251 let z_depth = entity_transform.translation().z;
252 let Some(root_entity) = root_resolver.resolve_root(entity) else {
253 continue;
254 };
255 camera_state.consider(root_entity, z_depth, || {
256 cursor.cursor_in_world_pos.extend(z_depth)
257 });
258 }
259 }
260 }
261}
262
263fn update_camera_status_for_2d_meshes(
264 mut cameras_query: Query<(&mut VpeolCameraState, &VisibleEntities)>,
265 entities_query: Query<(Entity, &GlobalTransform, &Mesh2d)>,
266 mesh_assets: Res<Assets<Mesh>>,
267 root_resolver: VpeolRootResolver,
268) {
269 for (mut camera_state, visible_entities) in cameras_query.iter_mut() {
270 let Some(cursor_ray) = camera_state.cursor_ray else {
271 continue;
272 };
273 for (entity, global_transform, mesh) in
274 entities_query.iter_many(visible_entities.iter(TypeId::of::<Mesh2d>()))
275 {
276 let Some(mesh) = mesh_assets.get(&mesh.0) else {
277 continue;
278 };
279
280 let inverse_transform = global_transform.to_matrix().inverse();
281
282 let ray_in_object_coords = Ray3d {
283 origin: inverse_transform.transform_point3(cursor_ray.origin),
284 direction: inverse_transform
285 .transform_vector3(*cursor_ray.direction)
286 .try_into()
287 .unwrap(),
288 };
289
290 let Some(distance) = ray_intersection_with_mesh(ray_in_object_coords, mesh) else {
291 continue;
292 };
293
294 let Some(root_entity) = root_resolver.resolve_root(entity) else {
295 continue;
296 };
297 camera_state.consider(root_entity, -distance, || cursor_ray.get_point(distance));
298 }
299 }
300}
301
302fn update_camera_status_for_text_2d(
303 mut cameras_query: Query<(&mut VpeolCameraState, &VisibleEntities)>,
304 entities_query: Query<(Entity, &GlobalTransform, &TextLayoutInfo, &Anchor)>,
305 root_resolver: VpeolRootResolver,
306) {
307 for (mut camera_state, visible_entities) in cameras_query.iter_mut() {
308 let Some(cursor) = CursorInWorldPos::from_camera_state(&camera_state) else {
309 continue;
310 };
311
312 for (entity, entity_transform, text_layout_info, anchor) in
313 entities_query.iter_many(visible_entities.iter(TypeId::of::<Sprite>()))
315 {
316 if cursor.check_square(entity_transform, anchor, text_layout_info.size) {
317 let z_depth = entity_transform.translation().z;
318 let Some(root_entity) = root_resolver.resolve_root(entity) else {
319 continue;
320 };
321 camera_state.consider(root_entity, z_depth, || {
322 cursor.cursor_in_world_pos.extend(z_depth)
323 });
324 }
325 }
326 }
327}
328
329#[derive(Component)]
331#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
332pub struct Vpeol2dCameraControl {
333 pub zoom_per_scroll_line: f32,
335 pub zoom_per_scroll_pixel: f32,
337}
338
339impl Default for Vpeol2dCameraControl {
340 fn default() -> Self {
341 Self {
342 zoom_per_scroll_line: 0.2,
343 zoom_per_scroll_pixel: 0.001,
344 }
345 }
346}
347
348fn camera_2d_pan(
349 mut egui_context: EguiContexts,
350 mouse_buttons: Res<ButtonInput<MouseButton>>,
351 mut cameras_query: Query<
352 (Entity, &mut Transform, &VpeolCameraState),
353 With<Vpeol2dCameraControl>,
354 >,
355 mut last_cursor_world_pos_by_camera: Local<HashMap<Entity, Vec2>>,
356) -> Result {
357 enum MouseButtonOp {
358 JustPressed,
359 BeingPressed,
360 }
361
362 let mouse_button_op = if mouse_buttons.just_pressed(MouseButton::Right) {
363 if egui_context.ctx_mut()?.is_pointer_over_area() {
364 return Ok(());
365 }
366 MouseButtonOp::JustPressed
367 } else if mouse_buttons.pressed(MouseButton::Right) {
368 MouseButtonOp::BeingPressed
369 } else {
370 last_cursor_world_pos_by_camera.clear();
371 return Ok(());
372 };
373
374 for (camera_entity, mut camera_transform, camera_state) in cameras_query.iter_mut() {
375 let Some(cursor_ray) = camera_state.cursor_ray else {
376 continue;
377 };
378 let world_pos = cursor_ray.origin.truncate();
379
380 match mouse_button_op {
381 MouseButtonOp::JustPressed => {
382 last_cursor_world_pos_by_camera.insert(camera_entity, world_pos);
383 }
384 MouseButtonOp::BeingPressed => {
385 if let Some(prev_pos) = last_cursor_world_pos_by_camera.get_mut(&camera_entity) {
386 let movement = *prev_pos - world_pos;
387 camera_transform.translation += movement.extend(0.0);
388 }
389 }
390 }
391 }
392 Ok(())
393}
394
395fn camera_2d_zoom(
396 mut egui_context: EguiContexts,
397 window_getter: WindowGetter,
398 mut cameras_query: Query<(
399 &mut Transform,
400 &VpeolCameraState,
401 &Camera,
402 &RenderTarget,
403 &Vpeol2dCameraControl,
404 )>,
405 mut wheel_events_reader: MessageReader<MouseWheel>,
406) -> Result {
407 if egui_context.ctx_mut()?.is_pointer_over_area() {
408 return Ok(());
409 }
410
411 for (mut camera_transform, camera_state, camera, render_target, camera_control) in
412 cameras_query.iter_mut()
413 {
414 let Some(cursor_ray) = camera_state.cursor_ray else {
415 continue;
416 };
417 let world_pos = cursor_ray.origin.truncate();
418
419 let zoom_amount: f32 = wheel_events_reader
420 .read()
421 .map(|wheel_event| match wheel_event.unit {
422 bevy::input::mouse::MouseScrollUnit::Line => {
423 wheel_event.y * camera_control.zoom_per_scroll_line
424 }
425 bevy::input::mouse::MouseScrollUnit::Pixel => {
426 wheel_event.y * camera_control.zoom_per_scroll_pixel
427 }
428 })
429 .sum();
430
431 if zoom_amount == 0.0 {
432 continue;
433 }
434
435 let scale_by = (-zoom_amount).exp();
436
437 let window = if let RenderTarget::Window(window_ref) = render_target {
438 window_getter.get_window(*window_ref).unwrap()
439 } else {
440 continue;
441 };
442 camera_transform.scale.x *= scale_by;
443 camera_transform.scale.y *= scale_by;
444 let Some(cursor_in_screen_pos) = window.cursor_position() else {
445 continue;
446 };
447 let Ok(new_cursor_ray) =
448 camera.viewport_to_world(&((*camera_transform.as_ref()).into()), cursor_in_screen_pos)
449 else {
450 continue;
451 };
452 let new_world_pos = new_cursor_ray.origin.truncate();
453 camera_transform.translation += (world_pos - new_world_pos).extend(0.0);
454 }
455 Ok(())
456}
457
458#[derive(Clone, PartialEq, Serialize, Deserialize, Component, Default, YoleckComponent)]
460#[serde(transparent)]
461#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
462pub struct Vpeol2dPosition(pub Vec2);
463
464#[derive(Default, Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]
468#[serde(transparent)]
469#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
470pub struct Vpeol2dRotatation(pub f32);
471
472#[derive(Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]
476#[serde(transparent)]
477#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
478pub struct Vpeol2dScale(pub Vec2);
479
480impl Default for Vpeol2dScale {
481 fn default() -> Self {
482 Self(Vec2::ONE)
483 }
484}
485
486fn vpeol_2d_edit_transform_group(
487 mut ui: ResMut<YoleckUi>,
488 position_edit: YoleckEdit<(Entity, &mut Vpeol2dPosition)>,
489 rotation_edit: YoleckEdit<&mut Vpeol2dRotatation>,
490 scale_edit: YoleckEdit<&mut Vpeol2dScale>,
491 passed_data: Res<YoleckPassedData>,
492) {
493 let has_any = !position_edit.is_empty() || !rotation_edit.is_empty() || !scale_edit.is_empty();
494 if !has_any {
495 return;
496 }
497
498 ui.group(|ui| {
499 ui.label(egui::RichText::new("Transform").strong());
500 ui.separator();
501
502 vpeol_2d_edit_position_impl(ui, position_edit, &passed_data);
503 vpeol_2d_edit_rotation_impl(ui, rotation_edit);
504 vpeol_2d_edit_scale_impl(ui, scale_edit);
505 });
506}
507
508fn vpeol_2d_edit_position_impl(
509 ui: &mut egui::Ui,
510 mut edit: YoleckEdit<(Entity, &mut Vpeol2dPosition)>,
511 passed_data: &YoleckPassedData,
512) {
513 if edit.is_empty() || edit.has_nonmatching() {
514 return;
515 }
516 let mut average = DVec2::ZERO;
517 let mut num_entities = 0;
518 let mut transition = Vec2::ZERO;
519 for (entity, position) in edit.iter_matching() {
520 if let Some(pos) = passed_data.get::<Vec3>(entity) {
521 transition = pos.truncate() - position.0;
522 }
523 average += position.0.as_dvec2();
524 num_entities += 1;
525 }
526 average /= num_entities as f64;
527
528 ui.horizontal(|ui| {
529 let mut new_average = average;
530
531 ui.add(egui::Label::new("Position"));
532 ui.add(egui::DragValue::new(&mut new_average.x).prefix("X:"));
533 ui.add(egui::DragValue::new(&mut new_average.y).prefix("Y:"));
534
535 transition += (new_average - average).as_vec2();
536 });
537
538 if transition.is_finite() && transition != Vec2::ZERO {
539 for (_, mut position) in edit.iter_matching_mut() {
540 position.0 += transition;
541 }
542 }
543}
544
545fn vpeol_2d_edit_rotation_impl(ui: &mut egui::Ui, mut edit: YoleckEdit<&mut Vpeol2dRotatation>) {
546 if edit.is_empty() || edit.has_nonmatching() {
547 return;
548 }
549
550 let mut average_rotation = 0.0_f32;
551 let mut num_entities = 0;
552
553 for rotation in edit.iter_matching() {
554 average_rotation += rotation.0;
555 num_entities += 1;
556 }
557 average_rotation /= num_entities as f32;
558
559 ui.horizontal(|ui| {
560 let mut rotation_deg = average_rotation.to_degrees();
561
562 ui.add(egui::Label::new("Rotation"));
563 ui.add(
564 egui::DragValue::new(&mut rotation_deg)
565 .speed(1.0)
566 .suffix("°"),
567 );
568
569 let new_rotation = rotation_deg.to_radians();
570 let transition = new_rotation - average_rotation;
571
572 if transition.is_finite() && transition != 0.0 {
573 for mut rotation in edit.iter_matching_mut() {
574 rotation.0 += transition;
575 }
576 }
577 });
578}
579
580fn vpeol_2d_edit_scale_impl(ui: &mut egui::Ui, mut edit: YoleckEdit<&mut Vpeol2dScale>) {
581 if edit.is_empty() || edit.has_nonmatching() {
582 return;
583 }
584 let mut average = DVec2::ZERO;
585 let mut num_entities = 0;
586
587 for scale in edit.iter_matching() {
588 average += scale.0.as_dvec2();
589 num_entities += 1;
590 }
591 average /= num_entities as f64;
592
593 ui.horizontal(|ui| {
594 let mut new_average = average;
595
596 ui.add(egui::Label::new("Scale"));
597 ui.vertical(|ui| {
598 ui.centered_and_justified(|ui| {
599 let axis_average = (average.x + average.y) / 2.0;
600 let mut new_axis_average = axis_average;
601 if ui
602 .add(egui::DragValue::new(&mut new_axis_average).speed(0.01))
603 .dragged()
604 {
605 let diff = new_axis_average - axis_average;
608 new_average.x += diff;
609 new_average.y += diff;
610 }
611 });
612 ui.horizontal(|ui| {
613 ui.add(
614 egui::DragValue::new(&mut new_average.x)
615 .prefix("X:")
616 .speed(0.01),
617 );
618 ui.add(
619 egui::DragValue::new(&mut new_average.y)
620 .prefix("Y:")
621 .speed(0.01),
622 );
623 });
624 });
625
626 let transition = (new_average - average).as_vec2();
627
628 if transition.is_finite() && transition != Vec2::ZERO {
629 for mut scale in edit.iter_matching_mut() {
630 scale.0 += transition;
631 }
632 }
633 });
634}
635
636fn vpeol_2d_init_position(
637 mut egui_context: EguiContexts,
638 ui: Res<YoleckUi>,
639 mut edit: YoleckEdit<&mut Vpeol2dPosition>,
640 cameras_query: Query<&VpeolCameraState>,
641 mouse_buttons: Res<ButtonInput<MouseButton>>,
642) -> YoleckExclusiveSystemDirective {
643 let Ok(mut position) = edit.single_mut() else {
644 return YoleckExclusiveSystemDirective::Finished;
645 };
646
647 let Some(cursor_ray) = cameras_query
648 .iter()
649 .find_map(|camera_state| camera_state.cursor_ray)
650 else {
651 return YoleckExclusiveSystemDirective::Listening;
652 };
653
654 position.0 = cursor_ray.origin.truncate();
655
656 if egui_context.ctx_mut().unwrap().is_pointer_over_area() || ui.ctx().is_pointer_over_area() {
657 return YoleckExclusiveSystemDirective::Listening;
658 }
659
660 if mouse_buttons.just_released(MouseButton::Left) {
661 return YoleckExclusiveSystemDirective::Finished;
662 }
663
664 YoleckExclusiveSystemDirective::Listening
665}
666
667fn vpeol_2d_populate_transform(
668 mut populate: YoleckPopulate<(
669 &Vpeol2dPosition,
670 Option<&Vpeol2dRotatation>,
671 Option<&Vpeol2dScale>,
672 &YoleckBelongsToLevel,
673 )>,
674 levels_query: Query<&VpeolRepositionLevel>,
675) {
676 populate.populate(
677 |_ctx, mut cmd, (position, rotation, scale, belongs_to_level)| {
678 let mut transform = Transform::from_translation(position.0.extend(0.0));
679 if let Some(Vpeol2dRotatation(rotation)) = rotation {
680 transform = transform.with_rotation(Quat::from_rotation_z(*rotation));
681 }
682 if let Some(Vpeol2dScale(scale)) = scale {
683 transform = transform.with_scale(scale.extend(1.0));
684 }
685
686 if let Ok(VpeolRepositionLevel(level_transform)) =
687 levels_query.get(belongs_to_level.level)
688 {
689 transform = *level_transform * transform;
690 }
691
692 cmd.insert((transform, GlobalTransform::from(transform)));
693 },
694 )
695}