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 entities_query
231 .iter_many(visible_entities.iter(TypeId::of::<Mesh2d>()))
235 .matched()
236 {
237 let size = if let Some(custom_size) = sprite.custom_size {
238 custom_size
239 } else if let Some(texture_atlas) = sprite.texture_atlas.as_ref() {
240 let Some(texture_atlas_layout) =
241 texture_atlas_layout_assets.get(&texture_atlas.layout)
242 else {
243 continue;
244 };
245 texture_atlas_layout.textures[texture_atlas.index]
246 .size()
247 .as_vec2()
248 } else if let Some(texture) = image_assets.get(&sprite.image) {
249 texture.size().as_vec2()
250 } else {
251 continue;
252 };
253 if cursor.check_square(entity_transform, anchor, size) {
254 let z_depth = entity_transform.translation().z;
255 let Some(root_entity) = root_resolver.resolve_root(entity) else {
256 continue;
257 };
258 camera_state.consider(root_entity, z_depth, || {
259 cursor.cursor_in_world_pos.extend(z_depth)
260 });
261 }
262 }
263 }
264}
265
266fn update_camera_status_for_2d_meshes(
267 mut cameras_query: Query<(&mut VpeolCameraState, &VisibleEntities)>,
268 entities_query: Query<(Entity, &GlobalTransform, &Mesh2d)>,
269 mesh_assets: Res<Assets<Mesh>>,
270 root_resolver: VpeolRootResolver,
271) {
272 for (mut camera_state, visible_entities) in cameras_query.iter_mut() {
273 let Some(cursor_ray) = camera_state.cursor_ray else {
274 continue;
275 };
276 for (entity, global_transform, mesh) in entities_query
277 .iter_many(visible_entities.iter(TypeId::of::<Mesh2d>()))
278 .matched()
279 {
280 let Some(mesh) = mesh_assets.get(&mesh.0) else {
281 continue;
282 };
283
284 let inverse_transform = global_transform.to_matrix().inverse();
285
286 let ray_in_object_coords = Ray3d {
287 origin: inverse_transform.transform_point3(cursor_ray.origin),
288 direction: inverse_transform
289 .transform_vector3(*cursor_ray.direction)
290 .try_into()
291 .unwrap(),
292 };
293
294 let Some(distance) = ray_intersection_with_mesh(ray_in_object_coords, mesh) else {
295 continue;
296 };
297
298 let Some(root_entity) = root_resolver.resolve_root(entity) else {
299 continue;
300 };
301 camera_state.consider(root_entity, -distance, || cursor_ray.get_point(distance));
302 }
303 }
304}
305
306fn update_camera_status_for_text_2d(
307 mut cameras_query: Query<(&mut VpeolCameraState, &VisibleEntities)>,
308 entities_query: Query<(Entity, &GlobalTransform, &TextLayoutInfo, &Anchor)>,
309 root_resolver: VpeolRootResolver,
310) {
311 for (mut camera_state, visible_entities) in cameras_query.iter_mut() {
312 let Some(cursor) = CursorInWorldPos::from_camera_state(&camera_state) else {
313 continue;
314 };
315
316 for (entity, entity_transform, text_layout_info, anchor) in
317 entities_query
319 .iter_many(visible_entities.iter(TypeId::of::<Sprite>()))
320 .matched()
321 {
322 if cursor.check_square(entity_transform, anchor, text_layout_info.size) {
323 let z_depth = entity_transform.translation().z;
324 let Some(root_entity) = root_resolver.resolve_root(entity) else {
325 continue;
326 };
327 camera_state.consider(root_entity, z_depth, || {
328 cursor.cursor_in_world_pos.extend(z_depth)
329 });
330 }
331 }
332 }
333}
334
335#[derive(Component)]
337#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
338pub struct Vpeol2dCameraControl {
339 pub zoom_per_scroll_line: f32,
341 pub zoom_per_scroll_pixel: f32,
343}
344
345impl Default for Vpeol2dCameraControl {
346 fn default() -> Self {
347 Self {
348 zoom_per_scroll_line: 0.2,
349 zoom_per_scroll_pixel: 0.001,
350 }
351 }
352}
353
354fn camera_2d_pan(
355 mut egui_context: EguiContexts,
356 mouse_buttons: Res<ButtonInput<MouseButton>>,
357 mut cameras_query: Query<
358 (Entity, &mut Transform, &VpeolCameraState),
359 With<Vpeol2dCameraControl>,
360 >,
361 mut last_cursor_world_pos_by_camera: Local<HashMap<Entity, Vec2>>,
362) -> Result {
363 enum MouseButtonOp {
364 JustPressed,
365 BeingPressed,
366 }
367
368 let mouse_button_op = if mouse_buttons.just_pressed(MouseButton::Right) {
369 if egui_context.ctx_mut()?.is_pointer_over_egui() {
370 return Ok(());
371 }
372 MouseButtonOp::JustPressed
373 } else if mouse_buttons.pressed(MouseButton::Right) {
374 MouseButtonOp::BeingPressed
375 } else {
376 last_cursor_world_pos_by_camera.clear();
377 return Ok(());
378 };
379
380 for (camera_entity, mut camera_transform, camera_state) in cameras_query.iter_mut() {
381 let Some(cursor_ray) = camera_state.cursor_ray else {
382 continue;
383 };
384 let world_pos = cursor_ray.origin.truncate();
385
386 match mouse_button_op {
387 MouseButtonOp::JustPressed => {
388 last_cursor_world_pos_by_camera.insert(camera_entity, world_pos);
389 }
390 MouseButtonOp::BeingPressed => {
391 if let Some(prev_pos) = last_cursor_world_pos_by_camera.get_mut(&camera_entity) {
392 let movement = *prev_pos - world_pos;
393 camera_transform.translation += movement.extend(0.0);
394 }
395 }
396 }
397 }
398 Ok(())
399}
400
401fn camera_2d_zoom(
402 mut egui_context: EguiContexts,
403 window_getter: WindowGetter,
404 mut cameras_query: Query<(
405 &mut Transform,
406 &VpeolCameraState,
407 &Camera,
408 &RenderTarget,
409 &Vpeol2dCameraControl,
410 )>,
411 mut wheel_events_reader: MessageReader<MouseWheel>,
412) -> Result {
413 if egui_context.ctx_mut()?.is_pointer_over_egui() {
414 return Ok(());
415 }
416
417 for (mut camera_transform, camera_state, camera, render_target, camera_control) in
418 cameras_query.iter_mut()
419 {
420 let Some(cursor_ray) = camera_state.cursor_ray else {
421 continue;
422 };
423 let world_pos = cursor_ray.origin.truncate();
424
425 let zoom_amount: f32 = wheel_events_reader
426 .read()
427 .map(|wheel_event| match wheel_event.unit {
428 bevy::input::mouse::MouseScrollUnit::Line => {
429 wheel_event.y * camera_control.zoom_per_scroll_line
430 }
431 bevy::input::mouse::MouseScrollUnit::Pixel => {
432 wheel_event.y * camera_control.zoom_per_scroll_pixel
433 }
434 })
435 .sum();
436
437 if zoom_amount == 0.0 {
438 continue;
439 }
440
441 let scale_by = (-zoom_amount).exp();
442
443 let window = if let RenderTarget::Window(window_ref) = render_target {
444 window_getter.get_window(*window_ref).unwrap()
445 } else {
446 continue;
447 };
448 camera_transform.scale.x *= scale_by;
449 camera_transform.scale.y *= scale_by;
450 let Some(cursor_in_screen_pos) = window.cursor_position() else {
451 continue;
452 };
453 let Ok(new_cursor_ray) =
454 camera.viewport_to_world(&((*camera_transform.as_ref()).into()), cursor_in_screen_pos)
455 else {
456 continue;
457 };
458 let new_world_pos = new_cursor_ray.origin.truncate();
459 camera_transform.translation += (world_pos - new_world_pos).extend(0.0);
460 }
461 Ok(())
462}
463
464#[derive(Clone, PartialEq, Serialize, Deserialize, Component, Default, YoleckComponent)]
466#[serde(transparent)]
467#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
468pub struct Vpeol2dPosition(pub Vec2);
469
470#[derive(Default, Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]
474#[serde(transparent)]
475#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
476pub struct Vpeol2dRotatation(pub f32);
477
478#[derive(Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]
482#[serde(transparent)]
483#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
484pub struct Vpeol2dScale(pub Vec2);
485
486impl Default for Vpeol2dScale {
487 fn default() -> Self {
488 Self(Vec2::ONE)
489 }
490}
491
492fn vpeol_2d_edit_transform_group(
493 mut ui: ResMut<YoleckUi>,
494 position_edit: YoleckEdit<(Entity, &mut Vpeol2dPosition)>,
495 rotation_edit: YoleckEdit<&mut Vpeol2dRotatation>,
496 scale_edit: YoleckEdit<&mut Vpeol2dScale>,
497 passed_data: Res<YoleckPassedData>,
498) {
499 let has_any = !position_edit.is_empty() || !rotation_edit.is_empty() || !scale_edit.is_empty();
500 if !has_any {
501 return;
502 }
503
504 ui.group(|ui| {
505 ui.label(egui::RichText::new("Transform").strong());
506 ui.separator();
507
508 vpeol_2d_edit_position_impl(ui, position_edit, &passed_data);
509 vpeol_2d_edit_rotation_impl(ui, rotation_edit);
510 vpeol_2d_edit_scale_impl(ui, scale_edit);
511 });
512}
513
514fn vpeol_2d_edit_position_impl(
515 ui: &mut egui::Ui,
516 mut edit: YoleckEdit<(Entity, &mut Vpeol2dPosition)>,
517 passed_data: &YoleckPassedData,
518) {
519 if edit.is_empty() || edit.has_nonmatching() {
520 return;
521 }
522 let mut average = DVec2::ZERO;
523 let mut num_entities = 0;
524 let mut transition = Vec2::ZERO;
525 for (entity, position) in edit.iter_matching() {
526 if let Some(pos) = passed_data.get::<Vec3>(entity) {
527 transition = pos.truncate() - position.0;
528 }
529 average += position.0.as_dvec2();
530 num_entities += 1;
531 }
532 average /= num_entities as f64;
533
534 ui.horizontal(|ui| {
535 let mut new_average = average;
536
537 ui.add(egui::Label::new("Position"));
538 ui.add(egui::DragValue::new(&mut new_average.x).prefix("X:"));
539 ui.add(egui::DragValue::new(&mut new_average.y).prefix("Y:"));
540
541 transition += (new_average - average).as_vec2();
542 });
543
544 if transition.is_finite() && transition != Vec2::ZERO {
545 for (_, mut position) in edit.iter_matching_mut() {
546 position.0 += transition;
547 }
548 }
549}
550
551fn vpeol_2d_edit_rotation_impl(ui: &mut egui::Ui, mut edit: YoleckEdit<&mut Vpeol2dRotatation>) {
552 if edit.is_empty() || edit.has_nonmatching() {
553 return;
554 }
555
556 let mut average_rotation = 0.0_f32;
557 let mut num_entities = 0;
558
559 for rotation in edit.iter_matching() {
560 average_rotation += rotation.0;
561 num_entities += 1;
562 }
563 average_rotation /= num_entities as f32;
564
565 ui.horizontal(|ui| {
566 let mut rotation_deg = average_rotation.to_degrees();
567
568 ui.add(egui::Label::new("Rotation"));
569 ui.add(
570 egui::DragValue::new(&mut rotation_deg)
571 .speed(1.0)
572 .suffix("°"),
573 );
574
575 let new_rotation = rotation_deg.to_radians();
576 let transition = new_rotation - average_rotation;
577
578 if transition.is_finite() && transition != 0.0 {
579 for mut rotation in edit.iter_matching_mut() {
580 rotation.0 += transition;
581 }
582 }
583 });
584}
585
586fn vpeol_2d_edit_scale_impl(ui: &mut egui::Ui, mut edit: YoleckEdit<&mut Vpeol2dScale>) {
587 if edit.is_empty() || edit.has_nonmatching() {
588 return;
589 }
590 let mut average = DVec2::ZERO;
591 let mut num_entities = 0;
592
593 for scale in edit.iter_matching() {
594 average += scale.0.as_dvec2();
595 num_entities += 1;
596 }
597 average /= num_entities as f64;
598
599 ui.horizontal(|ui| {
600 let mut new_average = average;
601
602 ui.add(egui::Label::new("Scale"));
603 ui.vertical(|ui| {
604 ui.centered_and_justified(|ui| {
605 let axis_average = (average.x + average.y) / 2.0;
606 let mut new_axis_average = axis_average;
607 if ui
608 .add(egui::DragValue::new(&mut new_axis_average).speed(0.01))
609 .dragged()
610 {
611 let diff = new_axis_average - axis_average;
614 new_average.x += diff;
615 new_average.y += diff;
616 }
617 });
618 ui.horizontal(|ui| {
619 ui.add(
620 egui::DragValue::new(&mut new_average.x)
621 .prefix("X:")
622 .speed(0.01),
623 );
624 ui.add(
625 egui::DragValue::new(&mut new_average.y)
626 .prefix("Y:")
627 .speed(0.01),
628 );
629 });
630 });
631
632 let transition = (new_average - average).as_vec2();
633
634 if transition.is_finite() && transition != Vec2::ZERO {
635 for mut scale in edit.iter_matching_mut() {
636 scale.0 += transition;
637 }
638 }
639 });
640}
641
642fn vpeol_2d_init_position(
643 mut egui_context: EguiContexts,
644 ui: Res<YoleckUi>,
645 mut edit: YoleckEdit<&mut Vpeol2dPosition>,
646 cameras_query: Query<&VpeolCameraState>,
647 mouse_buttons: Res<ButtonInput<MouseButton>>,
648) -> YoleckExclusiveSystemDirective {
649 let Ok(mut position) = edit.single_mut() else {
650 return YoleckExclusiveSystemDirective::Finished;
651 };
652
653 let Some(cursor_ray) = cameras_query
654 .iter()
655 .find_map(|camera_state| camera_state.cursor_ray)
656 else {
657 return YoleckExclusiveSystemDirective::Listening;
658 };
659
660 position.0 = cursor_ray.origin.truncate();
661
662 if egui_context.ctx_mut().unwrap().is_pointer_over_egui() || ui.ctx().is_pointer_over_egui() {
663 return YoleckExclusiveSystemDirective::Listening;
664 }
665
666 if mouse_buttons.just_released(MouseButton::Left) {
667 return YoleckExclusiveSystemDirective::Finished;
668 }
669
670 YoleckExclusiveSystemDirective::Listening
671}
672
673fn vpeol_2d_populate_transform(
674 mut populate: YoleckPopulate<(
675 &Vpeol2dPosition,
676 Option<&Vpeol2dRotatation>,
677 Option<&Vpeol2dScale>,
678 &YoleckBelongsToLevel,
679 )>,
680 levels_query: Query<&VpeolRepositionLevel>,
681) {
682 populate.populate(
683 |_ctx, mut cmd, (position, rotation, scale, belongs_to_level)| {
684 let mut transform = Transform::from_translation(position.0.extend(0.0));
685 if let Some(Vpeol2dRotatation(rotation)) = rotation {
686 transform = transform.with_rotation(Quat::from_rotation_z(*rotation));
687 }
688 if let Some(Vpeol2dScale(scale)) = scale {
689 transform = transform.with_scale(scale.extend(1.0));
690 }
691
692 if let Ok(VpeolRepositionLevel(level_transform)) =
693 levels_query.get(belongs_to_level.level)
694 {
695 transform = *level_transform * transform;
696 }
697
698 cmd.insert((transform, GlobalTransform::from(transform)));
699 },
700 )
701}