1use std::any::TypeId;
87
88use crate::bevy_egui::{egui, EguiContexts};
89use crate::exclusive_systems::{
90 YoleckEntityCreationExclusiveSystems, YoleckExclusiveSystemDirective,
91};
92use crate::vpeol::{
93 handle_clickable_children_system, ray_intersection_with_mesh, VpeolBasePlugin,
94 VpeolCameraState, VpeolDragPlane, VpeolRepositionLevel, VpeolRootResolver, VpeolSystemSet,
95 WindowGetter,
96};
97use bevy::input::mouse::MouseWheel;
98use bevy::math::DVec2;
99use bevy::platform::collections::HashMap;
100use bevy::prelude::*;
101use bevy::render::camera::RenderTarget;
102use bevy::render::view::VisibleEntities;
103use bevy::sprite::Anchor;
104use bevy::text::TextLayoutInfo;
105use serde::{Deserialize, Serialize};
106
107use crate::{prelude::*, YoleckSchedule};
108
109pub struct Vpeol2dPluginForGame;
111
112impl Plugin for Vpeol2dPluginForGame {
113 fn build(&self, app: &mut App) {
114 app.add_systems(
115 YoleckSchedule::OverrideCommonComponents,
116 vpeol_2d_populate_transform,
117 );
118 #[cfg(feature = "bevy_reflect")]
119 register_reflect_types(app);
120 }
121}
122
123#[cfg(feature = "bevy_reflect")]
124fn register_reflect_types(app: &mut App) {
125 app.register_type::<Vpeol2dPosition>();
126 app.register_type::<Vpeol2dRotatation>();
127 app.register_type::<Vpeol2dScale>();
128 app.register_type::<Vpeol2dCameraControl>();
129}
130
131pub struct Vpeol2dPluginForEditor;
138
139impl Plugin for Vpeol2dPluginForEditor {
140 fn build(&self, app: &mut App) {
141 app.add_plugins(VpeolBasePlugin);
142 app.add_plugins(Vpeol2dPluginForGame);
143 app.insert_resource(VpeolDragPlane::XY);
144
145 app.add_systems(
146 Update,
147 (
148 update_camera_status_for_sprites,
149 update_camera_status_for_2d_meshes,
150 update_camera_status_for_text_2d,
151 )
152 .in_set(VpeolSystemSet::UpdateCameraState),
153 );
154 app.add_systems(
155 PostUpdate, (camera_2d_pan, camera_2d_zoom).run_if(in_state(YoleckEditorState::EditorActive)),
157 );
158 app.add_systems(
159 Update,
160 (
161 ApplyDeferred,
162 handle_clickable_children_system::<
163 Or<(With<Sprite>, (With<TextLayoutInfo>, With<Anchor>))>,
164 (),
165 >,
166 ApplyDeferred,
167 )
168 .chain()
169 .run_if(in_state(YoleckEditorState::EditorActive)),
170 );
171 app.add_yoleck_edit_system(vpeol_2d_edit_position);
172 app.world_mut()
173 .resource_mut::<YoleckEntityCreationExclusiveSystems>()
174 .on_entity_creation(|queue| queue.push_back(vpeol_2d_init_position));
175 }
176}
177
178struct CursorInWorldPos {
179 cursor_in_world_pos: Vec2,
180}
181
182impl CursorInWorldPos {
183 fn from_camera_state(camera_state: &VpeolCameraState) -> Option<Self> {
184 Some(Self {
185 cursor_in_world_pos: camera_state.cursor_ray?.origin.truncate(),
186 })
187 }
188
189 fn cursor_in_entity_space(&self, transform: &GlobalTransform) -> Vec2 {
190 transform
191 .compute_matrix()
192 .inverse()
193 .project_point3(self.cursor_in_world_pos.extend(0.0))
194 .truncate()
195 }
196
197 fn check_square(
198 &self,
199 entity_transform: &GlobalTransform,
200 anchor: &Anchor,
201 size: Vec2,
202 ) -> bool {
203 let cursor = self.cursor_in_entity_space(entity_transform);
204 let anchor = anchor.as_vec();
205 let mut min_corner = Vec2::new(-0.5, -0.5) - anchor;
206 let mut max_corner = Vec2::new(0.5, 0.5) - anchor;
207 for corner in [&mut min_corner, &mut max_corner] {
208 corner.x *= size.x;
209 corner.y *= size.y;
210 }
211 min_corner.x <= cursor.x
212 && cursor.x <= max_corner.x
213 && min_corner.y <= cursor.y
214 && cursor.y <= max_corner.y
215 }
216}
217
218#[allow(clippy::type_complexity)]
219fn update_camera_status_for_sprites(
220 mut cameras_query: Query<(&mut VpeolCameraState, &VisibleEntities)>,
221 entities_query: Query<(Entity, &GlobalTransform, &Sprite)>,
222 image_assets: Res<Assets<Image>>,
223 texture_atlas_layout_assets: Res<Assets<TextureAtlasLayout>>,
224 root_resolver: VpeolRootResolver,
225) {
226 for (mut camera_state, visible_entities) in cameras_query.iter_mut() {
227 let Some(cursor) = CursorInWorldPos::from_camera_state(&camera_state) else {
228 continue;
229 };
230
231 for (entity, entity_transform, sprite) in
232 entities_query.iter_many(visible_entities.iter(TypeId::of::<Sprite>()))
233 {
235 let size = if let Some(custom_size) = sprite.custom_size {
236 custom_size
237 } else if let Some(texture_atlas) = sprite.texture_atlas.as_ref() {
238 let Some(texture_atlas_layout) =
239 texture_atlas_layout_assets.get(&texture_atlas.layout)
240 else {
241 continue;
242 };
243 texture_atlas_layout.textures[texture_atlas.index]
244 .size()
245 .as_vec2()
246 } else if let Some(texture) = image_assets.get(&sprite.image) {
247 texture.size().as_vec2()
248 } else {
249 continue;
250 };
251 if cursor.check_square(entity_transform, &sprite.anchor, size) {
252 let z_depth = entity_transform.translation().z;
253 let Some(root_entity) = root_resolver.resolve_root(entity) else {
254 continue;
255 };
256 camera_state.consider(root_entity, z_depth, || {
257 cursor.cursor_in_world_pos.extend(z_depth)
258 });
259 }
260 }
261 }
262}
263
264fn update_camera_status_for_2d_meshes(
265 mut cameras_query: Query<(&mut VpeolCameraState, &VisibleEntities)>,
266 entities_query: Query<(Entity, &GlobalTransform, &Mesh2d)>,
267 mesh_assets: Res<Assets<Mesh>>,
268 root_resolver: VpeolRootResolver,
269) {
270 for (mut camera_state, visible_entities) in cameras_query.iter_mut() {
271 let Some(cursor_ray) = camera_state.cursor_ray else {
272 continue;
273 };
274 for (entity, global_transform, mesh) in
275 entities_query.iter_many(visible_entities.iter(TypeId::of::<Mesh2d>()))
276 {
277 let Some(mesh) = mesh_assets.get(&mesh.0) else {
278 continue;
279 };
280
281 let inverse_transform = global_transform.compute_matrix().inverse();
282
283 let ray_in_object_coords = Ray3d {
284 origin: inverse_transform.transform_point3(cursor_ray.origin),
285 direction: inverse_transform
286 .transform_vector3(*cursor_ray.direction)
287 .try_into()
288 .unwrap(),
289 };
290
291 let Some(distance) = ray_intersection_with_mesh(ray_in_object_coords, mesh) else {
292 continue;
293 };
294
295 let Some(root_entity) = root_resolver.resolve_root(entity) else {
296 continue;
297 };
298 camera_state.consider(root_entity, -distance, || cursor_ray.get_point(distance));
299 }
300 }
301}
302
303fn update_camera_status_for_text_2d(
304 mut cameras_query: Query<(&mut VpeolCameraState, &VisibleEntities)>,
305 entities_query: Query<(Entity, &GlobalTransform, &TextLayoutInfo, &Anchor)>,
306 root_resolver: VpeolRootResolver,
307) {
308 for (mut camera_state, visible_entities) in cameras_query.iter_mut() {
309 let Some(cursor) = CursorInWorldPos::from_camera_state(&camera_state) else {
310 continue;
311 };
312
313 for (entity, entity_transform, text_layout_info, anchor) in
314 entities_query.iter_many(visible_entities.iter(TypeId::of::<Sprite>()))
316 {
317 if cursor.check_square(entity_transform, anchor, text_layout_info.size) {
318 let z_depth = entity_transform.translation().z;
319 let Some(root_entity) = root_resolver.resolve_root(entity) else {
320 continue;
321 };
322 camera_state.consider(root_entity, z_depth, || {
323 cursor.cursor_in_world_pos.extend(z_depth)
324 });
325 }
326 }
327 }
328}
329
330#[derive(Component)]
332#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
333pub struct Vpeol2dCameraControl {
334 pub zoom_per_scroll_line: f32,
336 pub zoom_per_scroll_pixel: f32,
338}
339
340impl Default for Vpeol2dCameraControl {
341 fn default() -> Self {
342 Self {
343 zoom_per_scroll_line: 0.2,
344 zoom_per_scroll_pixel: 0.001,
345 }
346 }
347}
348
349fn camera_2d_pan(
350 mut egui_context: EguiContexts,
351 mouse_buttons: Res<ButtonInput<MouseButton>>,
352 mut cameras_query: Query<
353 (Entity, &mut Transform, &VpeolCameraState),
354 With<Vpeol2dCameraControl>,
355 >,
356 mut last_cursor_world_pos_by_camera: Local<HashMap<Entity, Vec2>>,
357) {
358 enum MouseButtonOp {
359 JustPressed,
360 BeingPressed,
361 }
362
363 let mouse_button_op = if mouse_buttons.just_pressed(MouseButton::Right) {
364 if egui_context.ctx_mut().is_pointer_over_area() {
365 return;
366 }
367 MouseButtonOp::JustPressed
368 } else if mouse_buttons.pressed(MouseButton::Right) {
369 MouseButtonOp::BeingPressed
370 } else {
371 last_cursor_world_pos_by_camera.clear();
372 return;
373 };
374
375 for (camera_entity, mut camera_transform, camera_state) in cameras_query.iter_mut() {
376 let Some(cursor_ray) = camera_state.cursor_ray else {
377 continue;
378 };
379 let world_pos = cursor_ray.origin.truncate();
380
381 match mouse_button_op {
382 MouseButtonOp::JustPressed => {
383 last_cursor_world_pos_by_camera.insert(camera_entity, world_pos);
384 }
385 MouseButtonOp::BeingPressed => {
386 if let Some(prev_pos) = last_cursor_world_pos_by_camera.get_mut(&camera_entity) {
387 let movement = *prev_pos - world_pos;
388 camera_transform.translation += movement.extend(0.0);
389 }
390 }
391 }
392 }
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 &Vpeol2dCameraControl,
403 )>,
404 mut wheel_events_reader: EventReader<MouseWheel>,
405) {
406 if egui_context.ctx_mut().is_pointer_over_area() {
407 return;
408 }
409
410 for (mut camera_transform, camera_state, camera, camera_control) in cameras_query.iter_mut() {
411 let Some(cursor_ray) = camera_state.cursor_ray else {
412 continue;
413 };
414 let world_pos = cursor_ray.origin.truncate();
415
416 let zoom_amount: f32 = wheel_events_reader
417 .read()
418 .map(|wheel_event| match wheel_event.unit {
419 bevy::input::mouse::MouseScrollUnit::Line => {
420 wheel_event.y * camera_control.zoom_per_scroll_line
421 }
422 bevy::input::mouse::MouseScrollUnit::Pixel => {
423 wheel_event.y * camera_control.zoom_per_scroll_pixel
424 }
425 })
426 .sum();
427
428 if zoom_amount == 0.0 {
429 continue;
430 }
431
432 let scale_by = (-zoom_amount).exp();
433
434 let window = if let RenderTarget::Window(window_ref) = camera.target {
435 window_getter.get_window(window_ref).unwrap()
436 } else {
437 continue;
438 };
439 camera_transform.scale.x *= scale_by;
440 camera_transform.scale.y *= scale_by;
441 let Some(cursor_in_screen_pos) = window.cursor_position() else {
442 continue;
443 };
444 let Ok(new_cursor_ray) =
445 camera.viewport_to_world(&((*camera_transform.as_ref()).into()), cursor_in_screen_pos)
446 else {
447 continue;
448 };
449 let new_world_pos = new_cursor_ray.origin.truncate();
450 camera_transform.translation += (world_pos - new_world_pos).extend(0.0);
451 }
452}
453
454#[derive(Clone, PartialEq, Serialize, Deserialize, Component, Default, YoleckComponent)]
456#[serde(transparent)]
457#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
458pub struct Vpeol2dPosition(pub Vec2);
459
460#[derive(Default, Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]
464#[serde(transparent)]
465#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
466pub struct Vpeol2dRotatation(pub f32);
467
468#[derive(Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]
470#[serde(transparent)]
471#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
472pub struct Vpeol2dScale(pub Vec2);
473
474impl Default for Vpeol2dScale {
475 fn default() -> Self {
476 Self(Vec2::ONE)
477 }
478}
479
480fn vpeol_2d_edit_position(
481 mut ui: ResMut<YoleckUi>,
482 mut edit: YoleckEdit<(Entity, &mut Vpeol2dPosition)>,
483 passed_data: Res<YoleckPassedData>,
484) {
485 if edit.is_empty() || edit.has_nonmatching() {
486 return;
487 }
488 let mut average = DVec2::ZERO;
490 let mut num_entities = 0;
491 let mut transition = Vec2::ZERO;
492 for (entity, position) in edit.iter_matching() {
493 if let Some(pos) = passed_data.get::<Vec3>(entity) {
494 transition = pos.truncate() - position.0;
495 }
496 average += position.0.as_dvec2();
497 num_entities += 1;
498 }
499 average /= num_entities as f64;
500
501 ui.horizontal(|ui| {
502 let mut new_average = average;
503 ui.add(egui::DragValue::new(&mut new_average.x).prefix("X:"));
504 ui.add(egui::DragValue::new(&mut new_average.y).prefix("Y:"));
505 transition += (new_average - average).as_vec2();
506 });
507
508 if transition.is_finite() && transition != Vec2::ZERO {
509 for (_, mut position) in edit.iter_matching_mut() {
510 position.0 += transition;
511 }
512 }
513}
514
515fn vpeol_2d_init_position(
516 mut egui_context: EguiContexts,
517 ui: Res<YoleckUi>,
518 mut edit: YoleckEdit<&mut Vpeol2dPosition>,
519 cameras_query: Query<&VpeolCameraState>,
520 mouse_buttons: Res<ButtonInput<MouseButton>>,
521) -> YoleckExclusiveSystemDirective {
522 let Ok(mut position) = edit.single_mut() else {
523 return YoleckExclusiveSystemDirective::Finished;
524 };
525
526 let Some(cursor_ray) = cameras_query
527 .iter()
528 .find_map(|camera_state| camera_state.cursor_ray)
529 else {
530 return YoleckExclusiveSystemDirective::Listening;
531 };
532
533 position.0 = cursor_ray.origin.truncate();
534
535 if egui_context.ctx_mut().is_pointer_over_area() || ui.ctx().is_pointer_over_area() {
536 return YoleckExclusiveSystemDirective::Listening;
537 }
538
539 if mouse_buttons.just_released(MouseButton::Left) {
540 return YoleckExclusiveSystemDirective::Finished;
541 }
542
543 YoleckExclusiveSystemDirective::Listening
544}
545
546fn vpeol_2d_populate_transform(
547 mut populate: YoleckPopulate<(
548 &Vpeol2dPosition,
549 Option<&Vpeol2dRotatation>,
550 Option<&Vpeol2dScale>,
551 &YoleckBelongsToLevel,
552 )>,
553 levels_query: Query<&VpeolRepositionLevel>,
554) {
555 populate.populate(
556 |_ctx, mut cmd, (position, rotation, scale, belongs_to_level)| {
557 let mut transform = Transform::from_translation(position.0.extend(0.0));
558 if let Some(Vpeol2dRotatation(rotation)) = rotation {
559 transform = transform.with_rotation(Quat::from_rotation_z(*rotation));
560 }
561 if let Some(Vpeol2dScale(scale)) = scale {
562 transform = transform.with_scale(scale.extend(1.0));
563 }
564
565 if let Ok(VpeolRepositionLevel(level_transform)) =
566 levels_query.get(belongs_to_level.level)
567 {
568 transform = *level_transform * transform;
569 }
570
571 cmd.insert((transform, GlobalTransform::from(transform)));
572 },
573 )
574}