1use crate::actions::edit_vertex::EDIT_VERTEX_ACTION_ID;
2use crate::editor::RUSTERIX;
3use crate::hud::{Hud, HudMode};
4use crate::prelude::*;
5use MapEvent::*;
6use ToolEvent::*;
7use rusterix::Assets;
8use rusterix::prelude::*;
9use scenevm::GeoId;
10use std::str::FromStr;
11
12pub struct VertexTool {
13 id: TheId,
14 click_pos: Vec2<f32>,
15 click_pos_3d: Vec3<f32>,
16 click_ray_intersection_3d: Option<Vec3<f32>>,
18 click_selected: bool,
19 drag_changed: bool,
20 rectangle_undo_map: Map,
21 was_clicked: bool,
22
23 hud: Hud,
24}
25
26impl Tool for VertexTool {
27 fn new() -> Self
28 where
29 Self: Sized,
30 {
31 Self {
32 id: TheId::named("Vertex Tool"),
33 click_pos: Vec2::zero(),
34 click_pos_3d: Vec3::zero(),
35 click_ray_intersection_3d: None,
36 click_selected: false,
37 drag_changed: false,
38 rectangle_undo_map: Map::default(),
39 was_clicked: false,
40
41 hud: Hud::new(HudMode::Vertex),
42 }
43 }
44
45 fn id(&self) -> TheId {
46 self.id.clone()
47 }
48 fn info(&self) -> String {
49 fl!("tool_vertex")
50 }
51 fn icon_name(&self) -> String {
52 str!("dot-outline")
53 }
54 fn accel(&self) -> Option<char> {
55 Some('V')
56 }
57
58 fn help_url(&self) -> Option<String> {
59 Some("docs/creator/tools/vertex".to_string())
60 }
61
62 fn tool_event(
63 &mut self,
64 tool_event: ToolEvent,
65 _ui: &mut TheUI,
66 ctx: &mut TheContext,
67 project: &mut Project,
68 server_ctx: &mut ServerContext,
69 ) -> bool {
70 match tool_event {
71 Activate => {
72 server_ctx.curr_map_tool_type = MapToolType::Vertex;
73
74 if let Some(map) = project.get_map_mut(server_ctx) {
75 map.selected_linedefs.clear();
76 map.selected_sectors.clear();
77 }
78
79 ctx.ui.send(TheEvent::Custom(
80 TheId::named("Map Selection Changed"),
81 TheValue::Empty,
82 ));
83
84 return true;
85 }
86 DeActivate => {
87 server_ctx.curr_map_tool_type = MapToolType::General;
88 server_ctx.hover_cursor = None;
89 return true;
90 }
91 _ => {}
92 };
93 false
94 }
95
96 fn map_event(
97 &mut self,
98 map_event: MapEvent,
99 ui: &mut TheUI,
100 ctx: &mut TheContext,
101 map: &mut Map,
102 server_ctx: &mut ServerContext,
103 ) -> Option<ProjectUndoAtom> {
104 let mut undo_atom: Option<ProjectUndoAtom> = None;
105
106 match map_event {
107 MapKey(c) => {
108 match c {
109 '1'..='9' => map.subdivisions = (c as u8 - b'0') as f32,
110 '0' => map.subdivisions = 10.0,
111 _ => {}
112 }
113 crate::editor::RUSTERIX.write().unwrap().set_dirty();
114 }
115 MapClicked(coord) => {
116 if self.hud.clicked(coord.x, coord.y, map, ui, ctx, server_ctx) {
117 self.was_clicked = false;
118 crate::editor::RUSTERIX.write().unwrap().set_dirty();
119 return None;
120 }
121 self.was_clicked = true;
122
123 self.click_selected = false;
124 if server_ctx.hover.0.is_some() {
125 let mut changed = false;
126
127 map.selected_entity_item = None;
128
129 if ui.shift {
130 if let Some(v) = server_ctx.hover.0 {
132 if !map.selected_vertices.contains(&v) {
133 map.selected_vertices.push(v);
134 changed = true;
135 }
136 }
137 self.click_selected = true;
138 } else if ui.alt {
139 if let Some(v) = server_ctx.hover.0 {
141 map.selected_vertices.retain(|&selected| selected != v);
142 changed = true;
143 }
144 } else {
145 if let Some(v) = server_ctx.hover.0 {
147 map.selected_vertices = vec![v];
148 changed = true;
149 } else {
150 map.selected_vertices.clear();
151 changed = true;
152 }
153 self.click_selected = true;
154 }
155
156 if changed {
157 server_ctx.curr_action_id =
158 Some(Uuid::from_str(EDIT_VERTEX_ACTION_ID).unwrap());
159
160 ctx.ui.send(TheEvent::Custom(
161 TheId::named("Map Selection Changed"),
162 TheValue::Empty,
163 ));
164 }
165 } else {
166 if ui.shift {
167 if let Some(render_view) = ui.get_render_view("PolyView") {
169 if server_ctx.editor_view_mode == EditorViewMode::D2 {
170 let prev = map.clone();
171 let dim = *render_view.dim();
172 let grid_pos = server_ctx.local_to_map_grid(
173 Vec2::new(dim.width as f32, dim.height as f32),
174 Vec2::new(coord.x as f32, coord.y as f32),
175 map,
176 map.subdivisions,
177 );
178
179 let id = map.add_vertex_at(grid_pos.x, grid_pos.y);
180 map.selected_vertices = vec![id];
181
182 server_ctx.curr_action_id =
183 Some(Uuid::from_str(EDIT_VERTEX_ACTION_ID).unwrap());
184
185 ctx.ui.send(TheEvent::Custom(
186 TheId::named("Map Selection Changed"),
187 TheValue::Empty,
188 ));
189
190 undo_atom = Some(ProjectUndoAtom::MapEdit(
191 server_ctx.pc,
192 Box::new(prev),
193 Box::new(map.clone()),
194 ));
195 } else if let Some(pt) = server_ctx.hover_cursor_3d {
196 let prev = map.clone();
197
198 let id = map.add_vertex_at_3d(pt.x, pt.z, pt.y, false);
199 map.selected_vertices = vec![id];
200
201 server_ctx.curr_action_id =
202 Some(Uuid::from_str(EDIT_VERTEX_ACTION_ID).unwrap());
203
204 ctx.ui.send(TheEvent::Custom(
205 TheId::named("Map Selection Changed"),
206 TheValue::Empty,
207 ));
208
209 undo_atom = Some(ProjectUndoAtom::MapEdit(
210 server_ctx.pc,
211 Box::new(prev),
212 Box::new(map.clone()),
213 ));
214 }
215 }
216 }
217 }
218
219 self.click_pos = Vec2::new(coord.x as f32, coord.y as f32);
220 self.click_ray_intersection_3d = None;
221
222 if self.click_selected && !map.selected_vertices.is_empty() {
224 if let Some(vertex) = map.find_vertex(map.selected_vertices[0]) {
225 self.click_pos_3d = vertex.as_vec3_world();
227 } else {
228 self.click_pos_3d = server_ctx.geo_hit_pos;
229 }
230
231 if server_ctx.editor_view_mode != EditorViewMode::D2 {
233 if let Some(render_view) = ui.get_render_view("PolyView") {
234 let dim = *render_view.dim();
235 let screen_uv = [
236 coord.x as f32 / dim.width as f32,
237 coord.y as f32 / dim.height as f32,
238 ];
239
240 let rusterix = RUSTERIX.read().unwrap();
241 let ray = rusterix.client.camera_d3.create_ray(
242 Vec2::new(screen_uv[0], 1.0 - screen_uv[1]),
243 Vec2::new(dim.width as f32, dim.height as f32),
244 Vec2::zero(),
245 );
246 drop(rusterix);
247
248 let plane = server_ctx.gizmo_mode;
249 let plane_normal = match plane {
250 GizmoMode::XZ => Vec3::new(0.0, 1.0, 0.0),
251 GizmoMode::XY => Vec3::new(0.0, 0.0, 1.0),
252 GizmoMode::YZ => Vec3::new(1.0, 0.0, 0.0),
253 };
254
255 let denom: f32 = plane_normal.dot(ray.dir);
256 if denom.abs() > 0.0001 {
257 let t = (self.click_pos_3d - ray.origin).dot(plane_normal) / denom;
258 if t >= 0.0 {
259 self.click_ray_intersection_3d = Some(ray.origin + ray.dir * t);
260 }
261 }
262 }
263 }
264 } else {
265 self.click_pos_3d = server_ctx.geo_hit_pos;
266 }
267
268 self.rectangle_undo_map = map.clone();
269 }
270 MapDragged(coord) => {
271 if self.hud.dragged(coord.x, coord.y, map, ui, ctx, server_ctx) {
272 crate::editor::RUSTERIX.write().unwrap().set_dirty();
273 return None;
274 }
275
276 if self.click_selected {
277 if let Some(render_view) = ui.get_render_view("PolyView") {
279 let dim = *render_view.dim();
280
281 if server_ctx.editor_view_mode == EditorViewMode::D2 {
282 let click_pos = server_ctx.local_to_map_grid(
283 Vec2::new(dim.width as f32, dim.height as f32),
284 self.click_pos,
285 map,
286 map.subdivisions,
287 );
288 let drag_pos = server_ctx.local_to_map_grid(
289 Vec2::new(dim.width as f32, dim.height as f32),
290 Vec2::new(coord.x as f32, coord.y as f32),
291 map,
292 map.subdivisions,
293 );
294
295 let drag_delta = click_pos - drag_pos;
296 for vertex_id in &map.selected_vertices.clone() {
297 if let Some(original_vertex) =
298 self.rectangle_undo_map.find_vertex_mut(*vertex_id)
299 {
300 let new_pos = Vec2::new(
301 original_vertex.x - drag_delta.x,
302 original_vertex.y - drag_delta.y,
303 );
304 let grid_step = 1.0 / map.subdivisions.max(1.0);
305 let snapped_pos = Vec2::new(
306 (new_pos.x / grid_step).round() * grid_step,
307 (new_pos.y / grid_step).round() * grid_step,
308 );
309 map.update_vertex(*vertex_id, snapped_pos);
310 }
311 }
312 server_ctx.hover_cursor = Some(drag_pos);
313
314 if drag_delta.x != 0.0 || drag_delta.y != 0.0 {
315 self.drag_changed = true;
316 }
317 } else {
318 let drag_distance = self
321 .click_pos
322 .distance(Vec2::new(coord.x as f32, coord.y as f32));
323 if drag_distance < 5.0 {
324 crate::editor::RUSTERIX.write().unwrap().set_dirty();
325 return None;
326 }
327
328 let click_intersection = match self.click_ray_intersection_3d {
331 Some(pos) => pos,
332 None => {
333 crate::editor::RUSTERIX.write().unwrap().set_dirty();
334 return None;
335 }
336 };
337
338 let start_pos = self.click_pos_3d;
339 let plane = server_ctx.gizmo_mode;
340
341 if let Some(render_view) = ui.get_render_view("PolyView") {
343 let dim = *render_view.dim();
344 let screen_uv = [
345 coord.x as f32 / dim.width as f32,
346 coord.y as f32 / dim.height as f32,
347 ];
348
349 let rusterix = RUSTERIX.read().unwrap();
351 let ray = rusterix.client.camera_d3.create_ray(
352 Vec2::new(screen_uv[0], 1.0 - screen_uv[1]),
353 Vec2::new(dim.width as f32, dim.height as f32),
354 Vec2::zero(),
355 );
356 drop(rusterix);
357
358 let plane_normal = match plane {
360 GizmoMode::XZ => Vec3::new(0.0, 1.0, 0.0), GizmoMode::XY => Vec3::new(0.0, 0.0, 1.0), GizmoMode::YZ => Vec3::new(1.0, 0.0, 0.0), };
364
365 let denom: f32 = plane_normal.dot(ray.dir);
367
368 if denom.abs() > 0.0001 {
369 let t = (start_pos - ray.origin).dot(plane_normal) / denom;
370 if t >= 0.0 {
371 let current_pos = ray.origin + ray.dir * t;
372
373 let drag_delta = match plane {
376 GizmoMode::XZ => {
377 Vec3::new(
379 current_pos.x - click_intersection.x,
380 0.0,
381 current_pos.z - click_intersection.z,
382 )
383 }
384 GizmoMode::XY => {
385 Vec3::new(
387 current_pos.x - click_intersection.x,
388 current_pos.y - click_intersection.y,
389 0.0,
390 )
391 }
392 GizmoMode::YZ => {
393 Vec3::new(
395 0.0,
396 current_pos.y - click_intersection.y,
397 current_pos.z - click_intersection.z,
398 )
399 }
400 };
401
402 for vertex_id in &map.selected_vertices.clone() {
404 if let Some(original_vertex) =
405 self.rectangle_undo_map.find_vertex(*vertex_id)
406 {
407 let new_x = original_vertex.x + drag_delta.x;
412 let new_y = original_vertex.y + drag_delta.z;
413 let new_z = original_vertex.z + drag_delta.y;
414
415 let subdivisions = 1.0 / map.subdivisions;
417 let snapped_x =
418 (new_x / subdivisions).round() * subdivisions;
419 let snapped_y =
420 (new_y / subdivisions).round() * subdivisions;
421 let snapped_z =
422 (new_z / subdivisions).round() * subdivisions;
423
424 if let Some(vertex) =
425 map.find_vertex_mut(*vertex_id)
426 {
427 vertex.x = snapped_x;
428 vertex.y = snapped_y;
429 vertex.z = snapped_z;
430 }
431 }
432 }
433 if drag_delta.x != 0.0
434 || drag_delta.y != 0.0
435 || drag_delta.z != 0.0
436 {
437 self.drag_changed = true;
438 }
439 }
440 }
441 }
442 }
443 }
444 } else if let Some(render_view) = ui.get_render_view("PolyView") {
445 if !self.was_clicked {
446 return None;
447 }
448
449 let dim = *render_view.dim();
451
452 let click_pos = server_ctx.local_to_map_grid(
453 Vec2::new(dim.width as f32, dim.height as f32),
454 self.click_pos,
455 map,
456 map.subdivisions,
457 );
458 let drag_pos = server_ctx.local_to_map_grid(
459 Vec2::new(dim.width as f32, dim.height as f32),
460 Vec2::new(coord.x as f32, coord.y as f32),
461 map,
462 map.subdivisions,
463 );
464
465 let selection = if server_ctx.editor_view_mode == EditorViewMode::D2 {
466 let top_left =
467 Vec2::new(click_pos.x.min(drag_pos.x), click_pos.y.min(drag_pos.y));
468 let bottom_right =
469 Vec2::new(click_pos.x.max(drag_pos.x), click_pos.y.max(drag_pos.y));
470
471 let mut selection =
472 server_ctx.geometry_in_rectangle(top_left, bottom_right, map);
473 selection.1 = vec![];
474 selection.2 = vec![];
475 selection
476 } else {
477 let mut selection = (vec![], vec![], vec![]);
478
479 let click_pos = self.click_pos;
480 let drag_pos = Vec2::new(coord.x as f32, coord.y as f32);
481
482 let top_left =
483 Vec2::new(click_pos.x.min(drag_pos.x), click_pos.y.min(drag_pos.y));
484 let bottom_right =
485 Vec2::new(click_pos.x.max(drag_pos.x), click_pos.y.max(drag_pos.y));
486
487 let mut rusterix = RUSTERIX.write().unwrap();
488 rusterix.scene_handler.vm.set_active_vm(2);
489 let vertices = rusterix.scene_handler.vm.active_vm().pick_geo_ids_in_rect(
490 dim.width as u32,
491 dim.height as u32,
492 top_left,
493 bottom_right,
494 GeoId::Vertex(0),
495 false,
496 false,
497 );
498 for v in vertices {
499 if let GeoId::Vertex(v) = v {
500 selection.0.push(v);
501 }
502 }
503 rusterix.scene_handler.vm.set_active_vm(0);
504 selection
505 };
506
507 *map = self.rectangle_undo_map.clone();
508 map.curr_rectangle = Some((click_pos, drag_pos));
509
510 if ui.shift {
511 map.add_to_selection(selection.0, selection.1, selection.2);
513 } else if ui.alt {
514 map.remove_from_selection(selection.0, selection.1, selection.2);
516 } else {
517 map.selected_vertices = selection.0;
519 }
520 }
521 crate::editor::RUSTERIX.write().unwrap().set_dirty();
522 }
523 MapUp(_) => {
524 if self.click_selected {
525 if self.drag_changed {
526 undo_atom = Some(ProjectUndoAtom::MapEdit(
527 server_ctx.pc,
528 Box::new(self.rectangle_undo_map.clone()),
529 Box::new(map.clone()),
530 ));
531
532 ctx.ui.send(TheEvent::Custom(
533 TheId::named("Map Selection Changed"),
534 TheValue::Empty,
535 ));
536 }
537 } else if map.curr_rectangle.is_some() {
538 map.curr_rectangle = None;
539
540 ctx.ui.send(TheEvent::Custom(
541 TheId::named("Map Selection Changed"),
542 TheValue::Empty,
543 ));
544 }
545 self.drag_changed = false;
546 self.click_selected = false;
547 self.was_clicked = false;
548 }
549 MapHover(coord) => {
550 if self.hud.hovered(coord.x, coord.y, map, ui, ctx, server_ctx) {
551 crate::editor::RUSTERIX.write().unwrap().set_dirty();
552 return None;
553 }
554
555 if server_ctx.editor_view_mode == EditorViewMode::D2 {
556 if let Some(render_view) = ui.get_render_view("PolyView") {
557 let dim = *render_view.dim();
558 let h = server_ctx.geometry_at(
559 Vec2::new(dim.width as f32, dim.height as f32),
560 Vec2::new(coord.x as f32, coord.y as f32),
561 map,
562 );
563 server_ctx.hover.0 = h.0;
564
565 let cp = server_ctx.local_to_map_grid(
566 Vec2::new(dim.width as f32, dim.height as f32),
567 Vec2::new(coord.x as f32, coord.y as f32),
568 map,
569 map.subdivisions,
570 );
571
572 ctx.ui.send(TheEvent::Custom(
573 TheId::named("Cursor Pos Changed"),
574 TheValue::Float2(cp),
575 ));
576
577 server_ctx.hover_cursor = Some(cp);
578 }
579 } else {
580 if let Some(geo_id) = server_ctx.geo_hit {
581 match geo_id {
582 GeoId::Vertex(id) => {
583 server_ctx.hover = (Some(id), None, None);
584 }
585 _ => {
586 server_ctx.hover = (None, None, None);
587 }
588 }
589 } else {
590 server_ctx.hover = (None, None, None);
591 }
592
593 if let Some(cp) = server_ctx.hover_cursor {
594 ctx.ui.send(TheEvent::Custom(
595 TheId::named("Cursor Pos Changed"),
596 TheValue::Float2(cp),
597 ));
598 }
599 }
600 if let Some(v) = server_ctx.hover.0 {
601 if let Some(vertex) = map.find_vertex(v) {
602 ctx.ui.send(TheEvent::SetStatusText(
603 TheId::empty(),
604 format!(
605 "Vertex {}: (X: {:.2}, Y: {:.2}, Z: {:.2})",
606 v, vertex.x, vertex.z, vertex.y
607 ),
608 ));
609 }
610 } else {
611 ctx.ui
612 .send(TheEvent::SetStatusText(TheId::empty(), "".into()));
613 }
614 }
615 MapDelete => {
616 if !map.selected_vertices.is_empty() {
617 let prev = map.clone();
618 let vertices = map.selected_vertices.clone();
619
620 #[allow(clippy::useless_vec)]
621 map.delete_elements(&vertices, &vec![], &vec![]);
622 map.selected_vertices.clear();
623
624 undo_atom = Some(ProjectUndoAtom::MapEdit(
625 server_ctx.pc,
626 Box::new(prev),
627 Box::new(map.clone()),
628 ));
629 ctx.ui.send(TheEvent::Custom(
630 TheId::named("Map Selection Changed"),
631 TheValue::Empty,
632 ));
633 }
634 }
635 MapEscape => {
636 if !map.selected_vertices.is_empty() {
637 map.selected_vertices.clear();
638
639 ctx.ui.send(TheEvent::Custom(
640 TheId::named("Map Selection Changed"),
641 TheValue::Empty,
642 ));
643 }
644 self.was_clicked = false;
645 crate::editor::RUSTERIX.write().unwrap().set_dirty();
646 }
647 };
648 undo_atom
649 }
650
651 fn draw_hud(
652 &mut self,
653 buffer: &mut TheRGBABuffer,
654 map: &mut Map,
655 ctx: &mut TheContext,
656 server_ctx: &mut ServerContext,
657 assets: &Assets,
658 ) {
659 let id = if !map.selected_vertices.is_empty() {
660 Some(map.selected_vertices[0])
661 } else {
662 None
663 };
664 self.hud.draw(buffer, map, ctx, server_ctx, id, assets);
665 }
666}