1use std::any::TypeId;
85
86use crate::bevy_egui::{egui, EguiContexts};
87use crate::exclusive_systems::{
88 YoleckEntityCreationExclusiveSystems, YoleckExclusiveSystemDirective,
89};
90use crate::vpeol::{
91 handle_clickable_children_system, ray_intersection_with_mesh, VpeolBasePlugin,
92 VpeolCameraState, VpeolDragPlane, VpeolRepositionLevel, VpeolRootResolver, VpeolSystems,
93 WindowGetter,
94};
95use bevy::camera::visibility::VisibleEntities;
96use bevy::camera::RenderTarget;
97use bevy::input::mouse::MouseWheel;
98use bevy::math::DVec2;
99use bevy::platform::collections::HashMap;
100use bevy::prelude::*;
101use bevy::sprite::Anchor;
102use bevy::text::TextLayoutInfo;
103use serde::{Deserialize, Serialize};
104
105use crate::{prelude::*, YoleckSchedule};
106
107pub struct Vpeol2dPluginForGame;
109
110impl Plugin for Vpeol2dPluginForGame {
111 fn build(&self, app: &mut App) {
112 app.add_systems(
113 YoleckSchedule::OverrideCommonComponents,
114 vpeol_2d_populate_transform,
115 );
116 #[cfg(feature = "bevy_reflect")]
117 register_reflect_types(app);
118 }
119}
120
121#[cfg(feature = "bevy_reflect")]
122fn register_reflect_types(app: &mut App) {
123 app.register_type::<Vpeol2dPosition>();
124 app.register_type::<Vpeol2dRotatation>();
125 app.register_type::<Vpeol2dScale>();
126 app.register_type::<Vpeol2dCameraControl>();
127}
128
129pub struct Vpeol2dPluginForEditor;
136
137impl Plugin for Vpeol2dPluginForEditor {
138 fn build(&self, app: &mut App) {
139 app.add_plugins(VpeolBasePlugin);
140 app.add_plugins(Vpeol2dPluginForGame);
141 app.insert_resource(VpeolDragPlane::XY);
142
143 app.add_systems(
144 Update,
145 (
146 update_camera_status_for_sprites,
147 update_camera_status_for_2d_meshes,
148 update_camera_status_for_text_2d,
149 )
150 .in_set(VpeolSystems::UpdateCameraState),
151 );
152 app.add_systems(
153 PostUpdate, (camera_2d_pan, camera_2d_zoom).run_if(in_state(YoleckEditorState::EditorActive)),
155 );
156 app.add_systems(
157 Update,
158 (
159 ApplyDeferred,
160 handle_clickable_children_system::<
161 Or<(With<Sprite>, (With<TextLayoutInfo>, With<Anchor>))>,
162 (),
163 >,
164 ApplyDeferred,
165 )
166 .chain()
167 .run_if(in_state(YoleckEditorState::EditorActive)),
168 );
169 app.add_yoleck_edit_system(vpeol_2d_edit_position);
170 app.world_mut()
171 .resource_mut::<YoleckEntityCreationExclusiveSystems>()
172 .on_entity_creation(|queue| queue.push_back(vpeol_2d_init_position));
173 }
174}
175
176struct CursorInWorldPos {
177 cursor_in_world_pos: Vec2,
178}
179
180impl CursorInWorldPos {
181 fn from_camera_state(camera_state: &VpeolCameraState) -> Option<Self> {
182 Some(Self {
183 cursor_in_world_pos: camera_state.cursor_ray?.origin.truncate(),
184 })
185 }
186
187 fn cursor_in_entity_space(&self, transform: &GlobalTransform) -> Vec2 {
188 transform
189 .to_matrix()
190 .inverse()
191 .project_point3(self.cursor_in_world_pos.extend(0.0))
192 .truncate()
193 }
194
195 fn check_square(
196 &self,
197 entity_transform: &GlobalTransform,
198 anchor: &Anchor,
199 size: Vec2,
200 ) -> bool {
201 let cursor = self.cursor_in_entity_space(entity_transform);
202 let anchor = anchor.as_vec();
203 let mut min_corner = Vec2::new(-0.5, -0.5) - anchor;
204 let mut max_corner = Vec2::new(0.5, 0.5) - anchor;
205 for corner in [&mut min_corner, &mut max_corner] {
206 corner.x *= size.x;
207 corner.y *= size.y;
208 }
209 min_corner.x <= cursor.x
210 && cursor.x <= max_corner.x
211 && min_corner.y <= cursor.y
212 && cursor.y <= max_corner.y
213 }
214}
215
216#[allow(clippy::type_complexity)]
217fn update_camera_status_for_sprites(
218 mut cameras_query: Query<(&mut VpeolCameraState, &VisibleEntities)>,
219 entities_query: Query<(Entity, &GlobalTransform, &Sprite, &Anchor)>,
220 image_assets: Res<Assets<Image>>,
221 texture_atlas_layout_assets: Res<Assets<TextureAtlasLayout>>,
222 root_resolver: VpeolRootResolver,
223) {
224 for (mut camera_state, visible_entities) in cameras_query.iter_mut() {
225 let Some(cursor) = CursorInWorldPos::from_camera_state(&camera_state) else {
226 continue;
227 };
228
229 for (entity, entity_transform, sprite, anchor) in
230 entities_query.iter_many(visible_entities.iter(TypeId::of::<Sprite>()))
231 {
233 let size = if let Some(custom_size) = sprite.custom_size {
234 custom_size
235 } else if let Some(texture_atlas) = sprite.texture_atlas.as_ref() {
236 let Some(texture_atlas_layout) =
237 texture_atlas_layout_assets.get(&texture_atlas.layout)
238 else {
239 continue;
240 };
241 texture_atlas_layout.textures[texture_atlas.index]
242 .size()
243 .as_vec2()
244 } else if let Some(texture) = image_assets.get(&sprite.image) {
245 texture.size().as_vec2()
246 } else {
247 continue;
248 };
249 if cursor.check_square(entity_transform, &anchor, size) {
250 let z_depth = entity_transform.translation().z;
251 let Some(root_entity) = root_resolver.resolve_root(entity) else {
252 continue;
253 };
254 camera_state.consider(root_entity, z_depth, || {
255 cursor.cursor_in_world_pos.extend(z_depth)
256 });
257 }
258 }
259 }
260}
261
262fn update_camera_status_for_2d_meshes(
263 mut cameras_query: Query<(&mut VpeolCameraState, &VisibleEntities)>,
264 entities_query: Query<(Entity, &GlobalTransform, &Mesh2d)>,
265 mesh_assets: Res<Assets<Mesh>>,
266 root_resolver: VpeolRootResolver,
267) {
268 for (mut camera_state, visible_entities) in cameras_query.iter_mut() {
269 let Some(cursor_ray) = camera_state.cursor_ray else {
270 continue;
271 };
272 for (entity, global_transform, mesh) in
273 entities_query.iter_many(visible_entities.iter(TypeId::of::<Mesh2d>()))
274 {
275 let Some(mesh) = mesh_assets.get(&mesh.0) else {
276 continue;
277 };
278
279 let inverse_transform = global_transform.to_matrix().inverse();
280
281 let ray_in_object_coords = Ray3d {
282 origin: inverse_transform.transform_point3(cursor_ray.origin),
283 direction: inverse_transform
284 .transform_vector3(*cursor_ray.direction)
285 .try_into()
286 .unwrap(),
287 };
288
289 let Some(distance) = ray_intersection_with_mesh(ray_in_object_coords, mesh) else {
290 continue;
291 };
292
293 let Some(root_entity) = root_resolver.resolve_root(entity) else {
294 continue;
295 };
296 camera_state.consider(root_entity, -distance, || cursor_ray.get_point(distance));
297 }
298 }
299}
300
301fn update_camera_status_for_text_2d(
302 mut cameras_query: Query<(&mut VpeolCameraState, &VisibleEntities)>,
303 entities_query: Query<(Entity, &GlobalTransform, &TextLayoutInfo, &Anchor)>,
304 root_resolver: VpeolRootResolver,
305) {
306 for (mut camera_state, visible_entities) in cameras_query.iter_mut() {
307 let Some(cursor) = CursorInWorldPos::from_camera_state(&camera_state) else {
308 continue;
309 };
310
311 for (entity, entity_transform, text_layout_info, anchor) in
312 entities_query.iter_many(visible_entities.iter(TypeId::of::<Sprite>()))
314 {
315 if cursor.check_square(entity_transform, anchor, text_layout_info.size) {
316 let z_depth = entity_transform.translation().z;
317 let Some(root_entity) = root_resolver.resolve_root(entity) else {
318 continue;
319 };
320 camera_state.consider(root_entity, z_depth, || {
321 cursor.cursor_in_world_pos.extend(z_depth)
322 });
323 }
324 }
325 }
326}
327
328#[derive(Component)]
330#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
331pub struct Vpeol2dCameraControl {
332 pub zoom_per_scroll_line: f32,
334 pub zoom_per_scroll_pixel: f32,
336}
337
338impl Default for Vpeol2dCameraControl {
339 fn default() -> Self {
340 Self {
341 zoom_per_scroll_line: 0.2,
342 zoom_per_scroll_pixel: 0.001,
343 }
344 }
345}
346
347fn camera_2d_pan(
348 mut egui_context: EguiContexts,
349 mouse_buttons: Res<ButtonInput<MouseButton>>,
350 mut cameras_query: Query<
351 (Entity, &mut Transform, &VpeolCameraState),
352 With<Vpeol2dCameraControl>,
353 >,
354 mut last_cursor_world_pos_by_camera: Local<HashMap<Entity, Vec2>>,
355) -> Result {
356 enum MouseButtonOp {
357 JustPressed,
358 BeingPressed,
359 }
360
361 let mouse_button_op = if mouse_buttons.just_pressed(MouseButton::Right) {
362 if egui_context.ctx_mut()?.is_pointer_over_area() {
363 return Ok(());
364 }
365 MouseButtonOp::JustPressed
366 } else if mouse_buttons.pressed(MouseButton::Right) {
367 MouseButtonOp::BeingPressed
368 } else {
369 last_cursor_world_pos_by_camera.clear();
370 return Ok(());
371 };
372
373 for (camera_entity, mut camera_transform, camera_state) in cameras_query.iter_mut() {
374 let Some(cursor_ray) = camera_state.cursor_ray else {
375 continue;
376 };
377 let world_pos = cursor_ray.origin.truncate();
378
379 match mouse_button_op {
380 MouseButtonOp::JustPressed => {
381 last_cursor_world_pos_by_camera.insert(camera_entity, world_pos);
382 }
383 MouseButtonOp::BeingPressed => {
384 if let Some(prev_pos) = last_cursor_world_pos_by_camera.get_mut(&camera_entity) {
385 let movement = *prev_pos - world_pos;
386 camera_transform.translation += movement.extend(0.0);
387 }
388 }
389 }
390 }
391 Ok(())
392}
393
394fn camera_2d_zoom(
395 mut egui_context: EguiContexts,
396 window_getter: WindowGetter,
397 mut cameras_query: Query<(
398 &mut Transform,
399 &VpeolCameraState,
400 &Camera,
401 &Vpeol2dCameraControl,
402 )>,
403 mut wheel_events_reader: MessageReader<MouseWheel>,
404) -> Result {
405 if egui_context.ctx_mut()?.is_pointer_over_area() {
406 return Ok(());
407 }
408
409 for (mut camera_transform, camera_state, camera, camera_control) in cameras_query.iter_mut() {
410 let Some(cursor_ray) = camera_state.cursor_ray else {
411 continue;
412 };
413 let world_pos = cursor_ray.origin.truncate();
414
415 let zoom_amount: f32 = wheel_events_reader
416 .read()
417 .map(|wheel_event| match wheel_event.unit {
418 bevy::input::mouse::MouseScrollUnit::Line => {
419 wheel_event.y * camera_control.zoom_per_scroll_line
420 }
421 bevy::input::mouse::MouseScrollUnit::Pixel => {
422 wheel_event.y * camera_control.zoom_per_scroll_pixel
423 }
424 })
425 .sum();
426
427 if zoom_amount == 0.0 {
428 continue;
429 }
430
431 let scale_by = (-zoom_amount).exp();
432
433 let window = if let RenderTarget::Window(window_ref) = camera.target {
434 window_getter.get_window(window_ref).unwrap()
435 } else {
436 continue;
437 };
438 camera_transform.scale.x *= scale_by;
439 camera_transform.scale.y *= scale_by;
440 let Some(cursor_in_screen_pos) = window.cursor_position() else {
441 continue;
442 };
443 let Ok(new_cursor_ray) =
444 camera.viewport_to_world(&((*camera_transform.as_ref()).into()), cursor_in_screen_pos)
445 else {
446 continue;
447 };
448 let new_world_pos = new_cursor_ray.origin.truncate();
449 camera_transform.translation += (world_pos - new_world_pos).extend(0.0);
450 }
451 Ok(())
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().unwrap().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}