Skip to main content

rustapi/tools/
vertex.rs

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    /// The initial ray intersection point on the drag plane at click time
17    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                        // Add
131                        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                        // Subtract
140                        if let Some(v) = server_ctx.hover.0 {
141                            map.selected_vertices.retain(|&selected| selected != v);
142                            changed = true;
143                        }
144                    } else {
145                        // Replace
146                        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                        // Add a new vertex
168                        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                // For 3D dragging, use the actual vertex position if one is selected
223                if self.click_selected && !map.selected_vertices.is_empty() {
224                    if let Some(vertex) = map.find_vertex(map.selected_vertices[0]) {
225                        // Convert vertex storage to world 3D coords
226                        self.click_pos_3d = vertex.as_vec3_world();
227                    } else {
228                        self.click_pos_3d = server_ctx.geo_hit_pos;
229                    }
230
231                    // Compute initial ray intersection on the drag plane at click time
232                    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 we selected a vertex, drag means we move all selected vertices
278                    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                            // 3D Drag
319                            // Only start dragging after a minimum distance threshold
320                            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                            // Use the initial ray intersection as reference (not vertex position)
329                            // This prevents the "jump" when starting to drag
330                            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                            // Get current mouse ray and intersect with drag plane
342                            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                                // Get the camera ray for current mouse position
350                                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                                // Define plane normal based on gizmo mode
359                                let plane_normal = match plane {
360                                    GizmoMode::XZ => Vec3::new(0.0, 1.0, 0.0), // Horizontal plane
361                                    GizmoMode::XY => Vec3::new(0.0, 0.0, 1.0), // Front plane
362                                    GizmoMode::YZ => Vec3::new(1.0, 0.0, 0.0), // Side plane
363                                };
364
365                                // Ray-plane intersection
366                                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                                        // Calculate drag delta relative to initial click intersection
374                                        // (not the vertex position)
375                                        let drag_delta = match plane {
376                                            GizmoMode::XZ => {
377                                                // XZ plane: allow movement in X and Z, lock Y
378                                                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                                                // XY plane: allow movement in X and Y, lock Z
386                                                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                                                // YZ plane: allow movement in Y and Z, lock X
394                                                Vec3::new(
395                                                    0.0,
396                                                    current_pos.y - click_intersection.y,
397                                                    current_pos.z - click_intersection.z,
398                                                )
399                                            }
400                                        };
401
402                                        // Apply drag delta to all selected vertices
403                                        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                                                // Coordinate mapping:
408                                                // vertex.x = world X, vertex.y = world Z, vertex.z = world Y
409                                                // drag_delta is in world coords (x, y, z) where y is up
410                                                // update_vertex_3d takes (vertex_id, new_x, new_z, new_y)
411                                                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                                                // Snap to grid
416                                                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                    // Otherwise we treat it as rectangle selection
450                    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                        // Add
512                        map.add_to_selection(selection.0, selection.1, selection.2);
513                    } else if ui.alt {
514                        // Remove
515                        map.remove_from_selection(selection.0, selection.1, selection.2);
516                    } else {
517                        // Replace
518                        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}