Skip to main content

rustapi/
toollist.rs

1use crate::editor::{DOCKMANAGER, RUSTERIX, SCENEMANAGER, UNDOMANAGER};
2use crate::prelude::*;
3pub use crate::tools::rect::RectTool;
4use rusterix::Assets;
5use rusterix::D3Camera;
6use rusterix::PixelSource;
7use rusterix::chunkbuilder::terrain_generator::{TerrainConfig, TerrainGenerator};
8use scenevm::GeoId;
9
10pub struct ToolList {
11    pub server_time: TheTime,
12    pub render_button_text: String,
13
14    pub game_tools: Vec<Box<dyn Tool>>,
15    pub curr_game_tool: usize,
16
17    // Editor tools for dock editors
18    pub editor_tools: Vec<Box<dyn EditorTool>>,
19    pub curr_editor_tool: usize,
20    pub editor_mode: bool,
21}
22
23impl Default for ToolList {
24    fn default() -> Self {
25        Self::new()
26    }
27}
28
29impl ToolList {
30    fn collect_terrain_tile_overrides(map: &Map) -> FxHashMap<(i32, i32), PixelSource> {
31        match map.properties.get("tiles") {
32            Some(Value::TileOverrides(tiles)) => tiles.clone(),
33            _ => FxHashMap::default(),
34        }
35    }
36
37    fn collect_terrain_blend_overrides(
38        map: &Map,
39    ) -> FxHashMap<(i32, i32), (VertexBlendPreset, PixelSource)> {
40        match map.properties.get("blend_tiles") {
41            Some(Value::BlendOverrides(tiles)) => tiles.clone(),
42            _ => FxHashMap::default(),
43        }
44    }
45
46    fn changed_terrain_override_keys(old_map: &Map, new_map: &Map) -> FxHashSet<(i32, i32)> {
47        let old_tiles = Self::collect_terrain_tile_overrides(old_map);
48        let new_tiles = Self::collect_terrain_tile_overrides(new_map);
49        let old_blends = Self::collect_terrain_blend_overrides(old_map);
50        let new_blends = Self::collect_terrain_blend_overrides(new_map);
51
52        let mut keys = FxHashSet::default();
53        for k in old_tiles.keys() {
54            keys.insert(*k);
55        }
56        for k in new_tiles.keys() {
57            keys.insert(*k);
58        }
59        for k in old_blends.keys() {
60            keys.insert(*k);
61        }
62        for k in new_blends.keys() {
63            keys.insert(*k);
64        }
65
66        let mut changed = FxHashSet::default();
67        for key in keys {
68            if old_tiles.get(&key) != new_tiles.get(&key)
69                || old_blends.get(&key) != new_blends.get(&key)
70            {
71                changed.insert(key);
72            }
73        }
74        changed
75    }
76
77    fn apply_editor_rgba_mode(&mut self, ui: &mut TheUI, ctx: &mut TheContext) {
78        if !self.editor_mode || self.curr_editor_tool >= self.editor_tools.len() {
79            return;
80        }
81
82        if let Some(mode) = self.editor_tools[self.curr_editor_tool].rgba_view_mode()
83            && let Some(layout) = ui.get_rgba_layout("Tile Editor Dock RGBA Layout")
84            && let Some(rgba_view) = layout.rgba_view_mut().as_rgba_view()
85        {
86            let is_selection_mode = mode == TheRGBAViewMode::TileSelection;
87            rgba_view.set_mode(mode);
88            rgba_view.set_rectangular_selection(is_selection_mode);
89            layout.relayout(ctx);
90        }
91    }
92
93    pub fn new() -> Self {
94        let game_tools: Vec<Box<dyn Tool>> = vec![
95            Box::new(SelectionTool::new()),
96            Box::new(VertexTool::new()),
97            Box::new(LinedefTool::new()),
98            Box::new(SectorTool::new()),
99            Box::new(RectTool::new()),
100            Box::new(crate::tools::entity::EntityTool::new()),
101            // Box::new(RenderTool::new()),
102            // Box::new(TerrainTool::new()),
103            // Box::new(CodeTool::new()),
104            // Box::new(DataTool::new()),
105            // Box::new(TilesetTool::new()),
106            // Box::new(ConfigTool::new()),
107            // Box::new(InfoTool::new()),
108            Box::new(GameTool::new()),
109        ];
110        Self {
111            server_time: TheTime::default(),
112            render_button_text: "Finished".to_string(),
113
114            game_tools,
115            curr_game_tool: 2,
116
117            editor_tools: Vec::new(),
118            curr_editor_tool: 0,
119            editor_mode: false,
120        }
121    }
122
123    /// Build the UI
124    pub fn set_active_editor(&mut self, list: &mut dyn TheVLayoutTrait, ctx: &mut TheContext) {
125        list.clear();
126        ctx.ui.relayout = true;
127
128        if self.editor_mode {
129            // Show editor tools
130            for (index, tool) in self.editor_tools.iter().enumerate() {
131                let mut b = TheToolListButton::new(tool.id());
132
133                b.set_icon_name(tool.icon_name());
134                b.set_status_text(&tool.info());
135                if index == self.curr_editor_tool {
136                    b.set_state(TheWidgetState::Selected);
137                }
138                list.add_widget(Box::new(b));
139            }
140        } else {
141            // Show game tools
142            for (index, tool) in self.game_tools.iter().enumerate() {
143                let mut b = TheToolListButton::new(tool.id());
144
145                b.set_icon_name(tool.icon_name());
146                b.set_status_text(&tool.info());
147                if index == self.curr_game_tool {
148                    b.set_state(TheWidgetState::Selected);
149                }
150                list.add_widget(Box::new(b));
151            }
152        }
153    }
154
155    /// Switch to editor tools mode
156    pub fn set_editor_tools(
157        &mut self,
158        tools: Vec<Box<dyn EditorTool>>,
159        ui: &mut TheUI,
160        ctx: &mut TheContext,
161    ) {
162        self.editor_tools = tools;
163        self.curr_editor_tool = 0;
164        self.editor_mode = true;
165
166        // Activate first tool
167        if !self.editor_tools.is_empty() {
168            self.editor_tools[0].activate();
169            self.apply_editor_rgba_mode(ui, ctx);
170        }
171
172        // Update the toolbar
173        if let Some(list) = ui.get_vlayout("Tool List Layout") {
174            self.set_active_editor(list, ctx);
175        }
176    }
177
178    /// Switch back to game tools mode
179    pub fn set_game_tools(&mut self, ui: &mut TheUI, ctx: &mut TheContext) {
180        // Deactivate current editor tool
181        if self.editor_mode && self.curr_editor_tool < self.editor_tools.len() {
182            self.editor_tools[self.curr_editor_tool].deactivate();
183        }
184
185        self.editor_mode = false;
186        self.editor_tools.clear();
187
188        // Update the toolbar
189        if let Some(list) = ui.get_vlayout("Tool List Layout") {
190            self.set_active_editor(list, ctx);
191        }
192    }
193
194    #[allow(clippy::too_many_arguments)]
195    /// If the map has been changed, update its context and add an undo.
196    fn update_map_context(
197        &mut self,
198        _ui: &mut TheUI,
199        ctx: &mut TheContext,
200        project: &mut Project,
201        server_ctx: &mut ServerContext,
202        undo_atom: Option<ProjectUndoAtom>,
203    ) {
204        if let Some(undo_atom) = undo_atom {
205            if let Some(pc) = undo_atom.pc() {
206                if pc.is_region() {
207                    if server_ctx.editor_view_mode == EditorViewMode::D2
208                        && server_ctx.editing_surface.is_some()
209                    {
210                    } else {
211                        self.update_geometry_overlay_3d(project, server_ctx);
212                    }
213                    let mut used_incremental_terrain_update = false;
214                    if server_ctx.curr_map_tool_type == MapToolType::Rect
215                        && server_ctx.editor_view_mode != EditorViewMode::D2
216                        && let ProjectUndoAtom::MapEdit(_, old_map, new_map) = &undo_atom
217                    {
218                        let changed_keys = Self::changed_terrain_override_keys(old_map, new_map);
219
220                        if !changed_keys.is_empty() {
221                            let chunk_size = new_map.terrain.chunk_size.max(1);
222                            let mut dirty_chunks: FxHashSet<(i32, i32)> = FxHashSet::default();
223                            for (x, z) in changed_keys {
224                                let cx = x.div_euclid(chunk_size) * chunk_size;
225                                let cz = z.div_euclid(chunk_size) * chunk_size;
226                                dirty_chunks.insert((cx, cz));
227                            }
228
229                            let mut sm = SCENEMANAGER.write().unwrap();
230                            sm.update_map((**new_map).clone());
231                            sm.add_dirty(dirty_chunks.into_iter().collect());
232                            used_incremental_terrain_update = true;
233                        }
234                    }
235
236                    if !used_incremental_terrain_update {
237                        crate::utils::scenemanager_render_map(project, server_ctx);
238                    }
239                    crate::editor::RUSTERIX.write().unwrap().set_dirty();
240                }
241            }
242            UNDOMANAGER.write().unwrap().add_undo(undo_atom, ctx);
243        }
244    }
245
246    pub fn draw_hud(
247        &mut self,
248        buffer: &mut TheRGBABuffer,
249        map: &mut Map,
250        ctx: &mut TheContext,
251        server_ctx: &mut ServerContext,
252        assets: &Assets,
253    ) {
254        self.game_tools[self.curr_game_tool].draw_hud(buffer, map, ctx, server_ctx, assets);
255    }
256
257    #[allow(clippy::too_many_arguments)]
258    pub fn handle_event(
259        &mut self,
260        event: &TheEvent,
261        ui: &mut TheUI,
262        ctx: &mut TheContext,
263        project: &mut Project,
264        server_ctx: &mut ServerContext,
265    ) -> bool {
266        if self.editor_mode && self.curr_editor_tool < self.editor_tools.len() {
267            let should_forward_to_tool = match event {
268                // Keep tool switching and shortcuts handled by ToolList itself.
269                TheEvent::StateChanged(_, _) | TheEvent::KeyDown(_) => false,
270                TheEvent::Custom(id, _) if id.name == "Set Tool" => false,
271                _ => true,
272            };
273            if should_forward_to_tool {
274                return self.editor_tools[self.curr_editor_tool]
275                    .handle_event(event, ui, ctx, project, server_ctx);
276            }
277        }
278
279        let mut redraw = false;
280        match event {
281            TheEvent::IndexChanged(id, index) => {
282                if id.name == "Editor View Switch" {
283                    let prev_mode = server_ctx.editor_view_mode;
284                    let old = prev_mode.is_3d();
285
286                    // Persist region camera anchors before switching.
287                    if let Some(region) = project.get_region_ctx_mut(server_ctx) {
288                        if prev_mode == EditorViewMode::D2 {
289                            server_ctx.store_edit_view_2d_for_map(
290                                region.map.id,
291                                region.map.offset,
292                                region.map.grid_size,
293                            );
294                        } else {
295                            server_ctx.store_edit_view_for_map(
296                                region.map.id,
297                                prev_mode,
298                                region.editing_position_3d,
299                                region.editing_look_at_3d,
300                            );
301                            if prev_mode == EditorViewMode::Iso {
302                                let iso_scale =
303                                    crate::editor::EDITCAMERA.read().unwrap().iso_camera.scale();
304                                server_ctx
305                                    .store_edit_view_iso_scale_for_map(region.map.id, iso_scale);
306                            }
307                            match prev_mode {
308                                EditorViewMode::Iso => {
309                                    region.editing_position_iso_3d =
310                                        Some(region.editing_position_3d);
311                                    region.editing_look_at_iso_3d = Some(region.editing_look_at_3d);
312                                    let iso_scale = crate::editor::EDITCAMERA
313                                        .read()
314                                        .unwrap()
315                                        .iso_camera
316                                        .scale();
317                                    region.editing_iso_scale = Some(iso_scale);
318                                }
319                                EditorViewMode::Orbit => {
320                                    region.editing_position_orbit_3d =
321                                        Some(region.editing_position_3d);
322                                    region.editing_look_at_orbit_3d =
323                                        Some(region.editing_look_at_3d);
324                                    region.editing_orbit_distance = Some(
325                                        crate::editor::EDITCAMERA
326                                            .read()
327                                            .unwrap()
328                                            .orbit_camera
329                                            .distance,
330                                    );
331                                }
332                                EditorViewMode::FirstP => {
333                                    region.editing_position_firstp_3d =
334                                        Some(region.editing_position_3d);
335                                    region.editing_look_at_firstp_3d =
336                                        Some(region.editing_look_at_3d);
337                                }
338                                EditorViewMode::D2 => {}
339                            }
340                        }
341                    }
342
343                    server_ctx.editor_view_mode = EditorViewMode::from_index(*index as i32);
344                    let new_mode = server_ctx.editor_view_mode;
345                    let new = new_mode.is_3d();
346
347                    // Restore region camera anchor for the selected view mode.
348                    if let Some(region) = project.get_region_ctx_mut(server_ctx) {
349                        if new_mode == EditorViewMode::D2 {
350                            if let Some((offset, grid_size)) =
351                                server_ctx.load_edit_view_2d_for_map(region.map.id)
352                            {
353                                region.map.offset = offset;
354                                region.map.grid_size = grid_size;
355                            } else {
356                                server_ctx.center_map_at_grid_pos(
357                                    Vec2::zero(),
358                                    Vec2::new(0.0, -1.0),
359                                    &mut region.map,
360                                );
361                            }
362                        } else if let Some((pos, look)) =
363                            server_ctx.load_edit_view_for_map(region.map.id, new_mode)
364                        {
365                            region.editing_position_3d = pos;
366                            region.editing_look_at_3d = look;
367                            if new_mode == EditorViewMode::Iso
368                                && let Some(iso_scale) =
369                                    server_ctx.load_edit_view_iso_scale_for_map(region.map.id)
370                            {
371                                crate::editor::EDITCAMERA
372                                    .write()
373                                    .unwrap()
374                                    .iso_camera
375                                    .set_parameter_f32("scale", iso_scale);
376                            }
377                            if new_mode == EditorViewMode::Orbit
378                                && let Some(distance) = region.editing_orbit_distance
379                            {
380                                crate::editor::EDITCAMERA
381                                    .write()
382                                    .unwrap()
383                                    .orbit_camera
384                                    .set_parameter_f32("distance", distance);
385                            }
386                        } else {
387                            match new_mode {
388                                EditorViewMode::Iso => {
389                                    if let (Some(pos), Some(look)) = (
390                                        region.editing_position_iso_3d,
391                                        region.editing_look_at_iso_3d,
392                                    ) {
393                                        region.editing_position_3d = pos;
394                                        region.editing_look_at_3d = look;
395                                    }
396                                    if let Some(iso_scale) = region.editing_iso_scale {
397                                        crate::editor::EDITCAMERA
398                                            .write()
399                                            .unwrap()
400                                            .iso_camera
401                                            .set_parameter_f32("scale", iso_scale);
402                                    }
403                                }
404                                EditorViewMode::Orbit => {
405                                    if let (Some(pos), Some(look)) = (
406                                        region.editing_position_orbit_3d,
407                                        region.editing_look_at_orbit_3d,
408                                    ) {
409                                        region.editing_position_3d = pos;
410                                        region.editing_look_at_3d = look;
411                                    }
412                                    if let Some(distance) = region.editing_orbit_distance {
413                                        crate::editor::EDITCAMERA
414                                            .write()
415                                            .unwrap()
416                                            .orbit_camera
417                                            .set_parameter_f32("distance", distance);
418                                    }
419                                }
420                                EditorViewMode::FirstP => {
421                                    if let (Some(pos), Some(look)) = (
422                                        region.editing_position_firstp_3d,
423                                        region.editing_look_at_firstp_3d,
424                                    ) {
425                                        region.editing_position_3d = pos;
426                                        region.editing_look_at_3d = look;
427                                    }
428                                }
429                                EditorViewMode::D2 => {}
430                            }
431                        }
432                    }
433
434                    if let Some(editing_pos_buffer) = server_ctx.editing_pos_buffer {
435                        if let Some(region) = project.get_region_ctx_mut(server_ctx) {
436                            region.editing_position_3d = editing_pos_buffer;
437                        }
438                        server_ctx.editing_pos_buffer = None;
439                    }
440                    server_ctx.editing_surface = None;
441
442                    RUSTERIX.write().unwrap().client.scene.d2_static.clear();
443                    RUSTERIX.write().unwrap().client.scene.d2_dynamic.clear();
444
445                    if old != new {
446                        ctx.ui.send(TheEvent::Custom(
447                            TheId::named("Render SceneManager Map"),
448                            TheValue::Empty,
449                        ));
450                    } else if new {
451                        self.update_geometry_overlay_3d(project, server_ctx);
452                    }
453                    RUSTERIX.write().unwrap().set_dirty();
454
455                    ctx.ui.send(TheEvent::Custom(
456                        TheId::named("Update Action List"),
457                        TheValue::Empty,
458                    ));
459                }
460            }
461            TheEvent::KeyDown(TheValue::Char(c)) => {
462                if let Some(id) = &ctx.ui.focus {
463                    if id.name == "PolyView" {
464                        if let Some(map) = project.get_map_mut(server_ctx) {
465                            if *c == ',' {
466                                map.grid_size -= 2.0;
467                                return false;
468                            } else if *c == '.' {
469                                map.grid_size += 2.0;
470                                return false;
471                            }
472
473                            let undo_atom = self.get_current_tool().map_event(
474                                MapEvent::MapKey(*c),
475                                ui,
476                                ctx,
477                                map,
478                                server_ctx,
479                            );
480                            if undo_atom.is_some() {
481                                map.changed += 1;
482                            }
483                            self.update_map_context(ui, ctx, project, server_ctx, undo_atom);
484                        }
485
486                        if server_ctx.get_map_context() == MapContext::Region
487                            && !server_ctx.rotated_entities.is_empty()
488                            && let Some(region) = project.get_region_mut(&server_ctx.curr_region)
489                        {
490                            for (id, (_from, to)) in server_ctx.rotated_entities.drain() {
491                                if let Some(instance) = region.characters.get_mut(&id) {
492                                    instance.orientation = to;
493                                }
494                                if let Some(entity) =
495                                    region.map.entities.iter_mut().find(|e| e.creator_id == id)
496                                {
497                                    entity.orientation = to;
498                                }
499                            }
500                        } else {
501                            server_ctx.rotated_entities.clear();
502                        }
503                    }
504                }
505
506                let mut acc = !ui.focus_widget_supports_text_input(ctx);
507                if self.get_current_tool().id().name == "Game Tool"
508                    || ui.ctrl
509                    || ui.logo
510                    || ui.alt
511                    || server_ctx.game_input_mode
512                {
513                    acc = false;
514                }
515
516                if acc {
517                    /*
518                    if (*c == '-' || *c == '=' || *c == '+') && (ui.ctrl || ui.logo) {
519                        // Global Zoom In / Zoom Out
520                        if let Some(region) = project.get_region_mut(&server_ctx.curr_region) {
521                            if *c == '=' || *c == '+' {
522                                region.zoom += 0.2;
523                            } else {
524                                region.zoom -= 0.2;
525                            }
526                            region.zoom = region.zoom.clamp(1.0, 5.0);
527                            if let Some(layout) = ui.get_rgba_layout("Region Editor") {
528                                layout.set_zoom(region.zoom);
529                                layout.relayout(ctx);
530                            }
531                            if let Some(edit) = ui.get_text_line_edit("Editor Zoom") {
532                                edit.set_value(TheValue::Float(region.zoom));
533                            }
534                            return true;
535                        }
536                    }*/
537
538                    let mut tool_uuid = None;
539
540                    if self.editor_mode {
541                        // Check editor tool accelerators
542                        for tool in self.editor_tools.iter() {
543                            if let Some(acc) = tool.accel() {
544                                if acc.to_ascii_lowercase() == *c {
545                                    tool_uuid = Some(tool.id().uuid);
546                                    ctx.ui.set_widget_state(
547                                        self.editor_tools[self.curr_editor_tool].id().name,
548                                        TheWidgetState::None,
549                                    );
550                                    ctx.ui
551                                        .set_widget_state(tool.id().name, TheWidgetState::Selected);
552                                }
553                            }
554                        }
555                    } else {
556                        // Check game tool accelerators
557                        for tool in self.game_tools.iter() {
558                            if let Some(acc) = tool.accel() {
559                                if acc.to_ascii_lowercase() == *c {
560                                    tool_uuid = Some(tool.id().uuid);
561                                    ctx.ui.set_widget_state(
562                                        self.game_tools[self.curr_game_tool].id().name,
563                                        TheWidgetState::None,
564                                    );
565                                    ctx.ui
566                                        .set_widget_state(tool.id().name, TheWidgetState::Selected);
567                                }
568                            }
569                        }
570                    }
571
572                    if let Some(uuid) = tool_uuid {
573                        self.set_tool(uuid, ui, ctx, project, server_ctx);
574                    }
575                }
576            }
577            TheEvent::StateChanged(id, state) => {
578                if id.name == "Editor View Switch"
579                    && *state == TheWidgetState::Clicked
580                    && server_ctx.editor_view_mode == EditorViewMode::D2
581                    && server_ctx.editing_surface.is_some()
582                {
583                    // Re-clicking 2D while editing a profile/surface should exit surface mode.
584                    server_ctx.editing_surface = None;
585                    RUSTERIX.write().unwrap().client.scene.d2_static.clear();
586                    RUSTERIX.write().unwrap().client.scene.d2_dynamic.clear();
587                    RUSTERIX.write().unwrap().set_dirty();
588                    ctx.ui.send(TheEvent::Custom(
589                        TheId::named("Render SceneManager Map"),
590                        TheValue::Empty,
591                    ));
592                    ctx.ui.send(TheEvent::Custom(
593                        TheId::named("Update Action List"),
594                        TheValue::Empty,
595                    ));
596                    redraw = true;
597                }
598                if id.name.contains("Tool") && *state == TheWidgetState::Selected {
599                    if server_ctx.help_mode {
600                        if self.editor_mode {
601                            for tool in self.editor_tools.iter() {
602                                if tool.id().uuid == id.uuid {
603                                    if let Some(url) = tool.help_url() {
604                                        ctx.ui.send(TheEvent::Custom(
605                                            TheId::named("Show Help"),
606                                            TheValue::Text(url),
607                                        ));
608                                    }
609                                }
610                            }
611                        } else {
612                            for tool in self.game_tools.iter() {
613                                if tool.id().uuid == id.uuid {
614                                    if tool.id().uuid == id.uuid {
615                                        if let Some(url) = tool.help_url() {
616                                            ctx.ui.send(TheEvent::Custom(
617                                                TheId::named("Show Help"),
618                                                TheValue::Text(url),
619                                            ));
620                                        }
621                                    }
622                                }
623                            }
624                        }
625                    }
626
627                    redraw = self.set_tool(id.uuid, ui, ctx, project, server_ctx);
628                }
629            }
630            TheEvent::KeyCodeDown(TheValue::KeyCode(code)) => {
631                if let Some(id) = &ctx.ui.focus {
632                    if id.name == "PolyView" {
633                        if *code == TheKeyCode::Up {
634                            if let Some(map) = project.get_map_mut(server_ctx) {
635                                map.offset.y += 50.0;
636                            }
637                            return false;
638                        }
639                        if *code == TheKeyCode::Down {
640                            if let Some(map) = project.get_map_mut(server_ctx) {
641                                map.offset.y -= 50.0;
642                            }
643                            return false;
644                        }
645                        if *code == TheKeyCode::Left {
646                            if let Some(map) = project.get_map_mut(server_ctx) {
647                                map.offset.x -= 50.0;
648                            }
649                            return false;
650                        }
651                        if *code == TheKeyCode::Right {
652                            if let Some(map) = project.get_map_mut(server_ctx) {
653                                map.offset.x += 50.0;
654                            }
655                            return false;
656                        }
657                        if *code == TheKeyCode::Escape {
658                            if let Some(map) = project.get_map_mut(server_ctx) {
659                                if server_ctx.paste_clipboard.is_some() {
660                                    server_ctx.paste_clipboard = None;
661                                    return true;
662                                }
663
664                                let undo_atom = self.get_current_tool().map_event(
665                                    MapEvent::MapEscape,
666                                    ui,
667                                    ctx,
668                                    map,
669                                    server_ctx,
670                                );
671                                if undo_atom.is_some() {
672                                    map.changed += 1;
673                                }
674                                self.update_map_context(ui, ctx, project, server_ctx, undo_atom);
675                                if server_ctx.editor_view_mode != EditorViewMode::D2 {
676                                    self.update_geometry_overlay_3d(project, server_ctx);
677                                }
678                            }
679                        } else if *code == TheKeyCode::Delete {
680                            if let Some(map) = project.get_map_mut(server_ctx) {
681                                let undo_atom = self.get_current_tool().map_event(
682                                    MapEvent::MapDelete,
683                                    ui,
684                                    ctx,
685                                    map,
686                                    server_ctx,
687                                );
688                                if undo_atom.is_some() {
689                                    map.changed += 1;
690                                }
691                                self.update_map_context(ui, ctx, project, server_ctx, undo_atom);
692                                if server_ctx.editor_view_mode != EditorViewMode::D2 {
693                                    self.update_geometry_overlay_3d(project, server_ctx);
694                                }
695                            }
696                        }
697                    }
698                }
699            }
700            TheEvent::RenderViewClicked(id, coord) => {
701                if id.name == "PolyView" {
702                    if !server_ctx.game_mode && !server_ctx.game_input_mode {
703                        if let Some(map) = project.get_map_mut(server_ctx) {
704                            if coord.y > 20 {
705                                // Test for Paste operation
706                                if let Some(paste) = &server_ctx.paste_clipboard {
707                                    if let Some(hover) = server_ctx.hover_cursor {
708                                        let prev = map.clone();
709                                        let prev_counts = (
710                                            map.vertices.len(),
711                                            map.linedefs.len(),
712                                            map.sectors.len(),
713                                        );
714                                        map.paste_at_position(paste, hover);
715                                        let post_counts = (
716                                            map.vertices.len(),
717                                            map.linedefs.len(),
718                                            map.sectors.len(),
719                                        );
720                                        let inserted = post_counts != prev_counts;
721
722                                        if inserted {
723                                            if server_ctx.curr_map_tool_type == MapToolType::Vertex
724                                            {
725                                                map.selected_linedefs.clear();
726                                                map.selected_sectors.clear();
727                                            } else if server_ctx.curr_map_tool_type
728                                                == MapToolType::Linedef
729                                            {
730                                                map.selected_vertices.clear();
731                                                map.selected_sectors.clear();
732                                            } else if server_ctx.curr_map_tool_type
733                                                == MapToolType::Sector
734                                            {
735                                                map.selected_vertices.clear();
736                                                map.selected_linedefs.clear();
737                                            }
738
739                                            // Paste bypasses normal tool finalize paths; rebuild
740                                            // associations + surfaces so overlays and rendering
741                                            // use a fully consistent map immediately.
742                                            map.sanitize();
743                                            map.changed += 1;
744                                            server_ctx.paste_clipboard = None;
745
746                                            let undo_atom = ProjectUndoAtom::MapEdit(
747                                                server_ctx.pc,
748                                                Box::new(prev),
749                                                Box::new(map.clone()),
750                                            );
751
752                                            // We bypass normal tool click/drag flow during paste.
753                                            // Reset any stale drag state in the active tool so a
754                                            // following drag/up event cannot restore an old map snapshot.
755                                            let _ = self.get_current_tool().map_event(
756                                                MapEvent::MapUp(*coord),
757                                                ui,
758                                                ctx,
759                                                map,
760                                                server_ctx,
761                                            );
762
763                                            self.update_map_context(
764                                                ui,
765                                                ctx,
766                                                project,
767                                                server_ctx,
768                                                Some(undo_atom),
769                                            );
770                                            ctx.ui.send(TheEvent::Custom(
771                                                TheId::named("Map Selection Changed"),
772                                                TheValue::Empty,
773                                            ));
774                                            ctx.ui.send(TheEvent::Custom(
775                                                TheId::named("Render SceneManager Map"),
776                                                TheValue::Empty,
777                                            ));
778
779                                            return true;
780                                        }
781                                    }
782                                }
783                            }
784                        }
785
786                        if let Some(map) = project.get_map_mut(server_ctx) {
787                            let undo_atom = self.get_current_tool().map_event(
788                                MapEvent::MapClicked(*coord),
789                                ui,
790                                ctx,
791                                map,
792                                server_ctx,
793                            );
794                            if undo_atom.is_some() {
795                                map.changed += 1;
796                            }
797                            self.update_map_context(ui, ctx, project, server_ctx, undo_atom);
798
799                            if server_ctx.editor_view_mode != EditorViewMode::D2 {
800                                self.update_geometry_overlay_3d(project, server_ctx);
801                            }
802                            redraw = true;
803                        }
804                    } else {
805                        let current_map = RUSTERIX.read().unwrap().client.current_map.clone();
806                        for r in &mut project.regions {
807                            if r.map.name == current_map {
808                                self.get_current_tool().map_event(
809                                    MapEvent::MapClicked(*coord),
810                                    ui,
811                                    ctx,
812                                    &mut r.map,
813                                    server_ctx,
814                                );
815                            }
816                        }
817                    }
818                }
819            }
820            TheEvent::RenderViewDragged(id, coord) => {
821                if id.name == "PolyView" {
822                    if server_ctx.editor_view_mode == EditorViewMode::D2 {
823                        // Map dragging handled by tools.
824                    }
825
826                    if let Some(map) = project.get_map_mut(server_ctx) {
827                        let undo_atom = self.get_current_tool().map_event(
828                            MapEvent::MapDragged(*coord),
829                            ui,
830                            ctx,
831                            map,
832                            server_ctx,
833                        );
834                        if undo_atom.is_some() {
835                            map.changed += 1;
836                            // if server_ctx.get_map_context() == MapContext::Shader {
837                            // }
838                        }
839                        self.update_map_context(ui, ctx, project, server_ctx, undo_atom);
840
841                        if server_ctx.editor_view_mode != EditorViewMode::D2 {
842                            self.update_geometry_overlay_3d(project, server_ctx);
843                        }
844                    }
845
846                    redraw = true;
847                }
848            }
849            TheEvent::RenderViewUp(id, coord) => {
850                if id.name == "PolyView" {
851                    if let Some(map) = project.get_map_mut(server_ctx) {
852                        let undo_atom = self.get_current_tool().map_event(
853                            MapEvent::MapUp(*coord),
854                            ui,
855                            ctx,
856                            map,
857                            server_ctx,
858                        );
859
860                        if undo_atom.is_some() {
861                            map.changed += 1;
862                            map.update_surfaces();
863                        }
864                        self.update_map_context(ui, ctx, project, server_ctx, undo_atom);
865                        if server_ctx.editor_view_mode != EditorViewMode::D2 {
866                            self.update_geometry_overlay_3d(project, server_ctx);
867                        }
868                    }
869
870                    if server_ctx.get_map_context() == MapContext::Region {
871                        if let Some(region) = project.get_region_mut(&server_ctx.curr_region) {
872                            let mut move_atoms: Vec<ProjectUndoAtom> = Vec::new();
873
874                            for (id, (from, to)) in server_ctx.moved_entities.drain() {
875                                if from != to {
876                                    if let Some(instance) = region.characters.get_mut(&id) {
877                                        instance.position = to;
878                                    }
879                                    move_atoms.push(ProjectUndoAtom::MoveRegionCharacterInstance(
880                                        server_ctx.curr_region,
881                                        id,
882                                        from,
883                                        to,
884                                    ));
885                                }
886                            }
887                            for (id, (from, to)) in server_ctx.moved_items.drain() {
888                                if from != to {
889                                    if let Some(instance) = region.items.get_mut(&id) {
890                                        instance.position = to;
891                                    }
892                                    move_atoms.push(ProjectUndoAtom::MoveRegionItemInstance(
893                                        server_ctx.curr_region,
894                                        id,
895                                        from,
896                                        to,
897                                    ));
898                                }
899                            }
900
901                            for atom in move_atoms {
902                                UNDOMANAGER.write().unwrap().add_undo(atom, ctx);
903                            }
904                        }
905                    } else {
906                        server_ctx.moved_entities.clear();
907                        server_ctx.moved_items.clear();
908                    }
909
910                    redraw = true;
911                }
912            }
913            TheEvent::RenderViewHoverChanged(id, coord) => {
914                if id.name == "PolyView" {
915                    if server_ctx.editor_view_mode != EditorViewMode::D2 {
916                        if let Some(render_view) = ui.get_render_view("PolyView") {
917                            if let Some(rc) = self.get_geometry_hit(render_view, *coord, server_ctx)
918                            {
919                                server_ctx.geo_hit = Some(rc.0);
920                                server_ctx.geo_hit_pos = rc.1;
921                            } else {
922                                server_ctx.geo_hit = None;
923                                server_ctx.geo_hit_pos = Vec3::zero();
924                            }
925                            // println!("{:?}", server_ctx.geo_hit);
926                        }
927                    }
928                    if let Some(map) = project.get_map_mut(server_ctx) {
929                        let undo_atom = self.get_current_tool().map_event(
930                            MapEvent::MapHover(*coord),
931                            ui,
932                            ctx,
933                            map,
934                            server_ctx,
935                        );
936                        if undo_atom.is_some() {
937                            map.changed += 1;
938                            map.update_surfaces();
939                        }
940                        self.update_map_context(ui, ctx, project, server_ctx, undo_atom);
941
942                        if server_ctx.editor_view_mode != EditorViewMode::D2 {
943                            self.update_geometry_overlay_3d(project, server_ctx);
944                        }
945                    }
946                    redraw = false;
947                }
948            }
949            // TheEvent::RenderViewScrollBy(id, coord) => { TODO
950            //     if id.name == "PolyView" {
951            //         if server_ctx.editor_view_mode == EditorViewMode::Iso {
952            //             if ui.ctrl || ui.logo {
953            //                 EDITCAMERA.write().unwrap().scroll_by(coord.y as f32);
954            //             }
955            //         }
956            //     }
957            // }
958            /*
959            TheEvent::TileEditorClicked(id, coord) => {
960                if id.name == "Region Editor View"
961                    || id.name == "Screen Editor View"
962                    || id.name == "TerrainMap View"
963                {
964                    let mut coord_f = Vec2f::from(*coord);
965                    if id.name == "Region Editor View" {
966                        if let Some(editor) = ui.get_rgba_layout("Region Editor") {
967                            if let Some(rgba_view) = editor.rgba_view_mut().as_rgba_view() {
968                                coord_f = rgba_view.float_pos();
969                            }
970                        }
971                    }
972
973                    self.get_current_tool().tool_event(
974                        ToolEvent::TileDown(*coord, coord_f),
975                        ToolContext::TwoD,
976                        ui,
977                        ctx,
978                        project,
979                        server,
980                        client,
981                        server_ctx,
982                    );
983                }
984            }
985            TheEvent::TileEditorDragged(id, coord) => {
986                if id.name == "Region Editor View"
987                    || id.name == "Screen Editor View"
988                    || id.name == "TerrainMap View"
989                {
990                    let mut coord_f = Vec2f::from(*coord);
991                    if id.name == "Region Editor View" {
992                        if let Some(editor) = ui.get_rgba_layout("Region Editor") {
993                            if let Some(rgba_view) = editor.rgba_view_mut().as_rgba_view() {
994                                coord_f = rgba_view.float_pos();
995                            }
996                        }
997                    }
998
999                    self.get_current_tool().tool_event(
1000                        ToolEvent::TileDrag(*coord, coord_f),
1001                        ToolContext::TwoD,
1002                        ui,
1003                        ctx,
1004                        project,
1005                        server,
1006                        client,
1007                        server_ctx,
1008                    );
1009                }
1010            }
1011            TheEvent::TileEditorUp(id) => {
1012                if id.name == "Region Editor View"
1013                    || id.name == "Screen Editor View"
1014                    || id.name == "TerrainMap View"
1015                {
1016                    self.get_current_tool().tool_event(
1017                        ToolEvent::TileUp,
1018                        ToolContext::TwoD,
1019                        ui,
1020                        ctx,
1021                        project,
1022                        server,
1023                        client,
1024                        server_ctx,
1025                    );
1026                }
1027            }
1028            TheEvent::RenderViewClicked(id, coord) => {
1029                if id.name == "PolyView" {
1030                    // if let Some(render_view) = ui.get_render_view("PolyView") {
1031                    // let dim = render_view.dim();
1032                    // if let Some(region) = project.get_region_mut(&server_ctx.curr_region) {
1033                    // let pos = RENDERER.lock().unwrap().get_hit_position_at(
1034                    //     *coord,
1035                    //     region,
1036                    //     &mut server.get_instance_draw_settings(server_ctx.curr_region),
1037                    //     dim.width as usize,
1038                    //     dim.height as usize,
1039                    // );
1040                    //
1041                    let pos = Some((*coord, *coord));
1042
1043                    if let Some((pos, _)) = pos {
1044                        redraw = self.get_current_tool().tool_event(
1045                            ToolEvent::TileDown(
1046                                vec2i(pos.x, pos.y),
1047                                vec2f(pos.x as f32, pos.y as f32),
1048                            ),
1049                            ToolContext::ThreeD,
1050                            ui,
1051                            ctx,
1052                            project,
1053                            server,
1054                            client,
1055                            server_ctx,
1056                        );
1057                    }
1058                    // }
1059                    // }
1060                }
1061            }
1062            TheEvent::RenderViewDragged(id, coord) => {
1063                if id.name == "PolyView" {
1064                    //if let Some(render_view) = ui.get_render_view("RenderView") {
1065                    //let dim = render_view.dim();
1066                    //if let Some(region) = project.get_region_mut(&server_ctx.curr_region) {
1067                    // let pos = RENDERER.lock().unwrap().get_hit_position_at(
1068                    //     *coord,
1069                    //     region,
1070                    //     &mut server.get_instance_draw_settings(server_ctx.curr_region),
1071                    //     dim.width as usize,
1072                    //     dim.height as usize,
1073                    // );
1074
1075                    let pos = Some((*coord, *coord));
1076
1077                    if let Some((pos, _)) = pos {
1078                        redraw = self.get_current_tool().tool_event(
1079                            ToolEvent::TileDrag(
1080                                vec2i(pos.x, pos.y),
1081                                vec2f(pos.x as f32, pos.y as f32),
1082                            ),
1083                            ToolContext::ThreeD,
1084                            ui,
1085                            ctx,
1086                            project,
1087                            server,
1088                            client,
1089                            server_ctx,
1090                        );
1091                    }
1092                    //}
1093                    //}
1094                }
1095            }*/
1096            // TheEvent::ContextMenuSelected(widget_id, item_id) => {
1097            //     if widget_id.name == "Render Button" {
1098            //         if let Some(region) = project.get_region_mut(&server_ctx.curr_region) {
1099            //             if item_id.name == "Start Renderer" {
1100            //                 PRERENDERTHREAD.lock().unwrap().set_paused(false);
1101            //             } else if item_id.name == "Pause Renderer" {
1102            //                 PRERENDERTHREAD.lock().unwrap().set_paused(true);
1103            //             } else if item_id.name == "Restart Renderer" {
1104            //                 PRERENDERTHREAD.lock().unwrap().set_paused(false);
1105            //                 PRERENDERTHREAD
1106            //                     .lock()
1107            //                     .unwrap()
1108            //                     .render_region(region.clone(), None);
1109            //             }
1110            //             redraw = true;
1111            //         }
1112            //     }
1113            // }
1114            TheEvent::Custom(id, value) => {
1115                if id.name == "Set Tool" {
1116                    if let TheValue::Text(name) = value {
1117                        if let Some(tool_id) = self.get_game_tool_uuid_of_name(name) {
1118                            self.set_tool(tool_id, ui, ctx, project, server_ctx);
1119                            ctx.ui
1120                                .set_widget_state(name.into(), TheWidgetState::Selected);
1121                        }
1122                    }
1123                }
1124            }
1125            _ => {}
1126        }
1127
1128        if !redraw {
1129            redraw = self
1130                .get_current_tool()
1131                .handle_event(event, ui, ctx, project, server_ctx);
1132        }
1133
1134        redraw
1135    }
1136
1137    /// Returns the curently active tool.
1138    pub fn get_current_tool(&mut self) -> &mut Box<dyn Tool> {
1139        &mut self.game_tools[self.curr_game_tool]
1140    }
1141
1142    /// Returns the curent editor tool.
1143    pub fn get_current_editor_tool(&mut self) -> &mut Box<dyn EditorTool> {
1144        &mut self.editor_tools[self.curr_editor_tool]
1145    }
1146
1147    #[allow(clippy::too_many_arguments)]
1148    pub fn deactivte_tool(
1149        &mut self,
1150        ui: &mut TheUI,
1151        ctx: &mut TheContext,
1152        project: &mut Project,
1153        server_ctx: &mut ServerContext,
1154    ) {
1155        self.game_tools[self.curr_game_tool].tool_event(
1156            ToolEvent::DeActivate,
1157            ui,
1158            ctx,
1159            project,
1160            server_ctx,
1161        );
1162    }
1163
1164    #[allow(clippy::too_many_arguments)]
1165    pub fn set_tool(
1166        &mut self,
1167        tool_id: Uuid,
1168        ui: &mut TheUI,
1169        ctx: &mut TheContext,
1170        project: &mut Project,
1171        server_ctx: &mut ServerContext,
1172    ) -> bool {
1173        let mut redraw = false;
1174        let mut switched_tool = false;
1175        let layout_name = "Game Tool Params";
1176        let mut old_tool_index = 0;
1177
1178        if self.editor_mode {
1179            // Handle editor tool switching
1180            for (index, tool) in self.editor_tools.iter().enumerate() {
1181                if tool.id().uuid == tool_id && index != self.curr_editor_tool {
1182                    switched_tool = true;
1183                    old_tool_index = self.curr_editor_tool;
1184                    self.curr_editor_tool = index;
1185                    redraw = true;
1186                }
1187            }
1188            if switched_tool {
1189                for (index, tool) in self.editor_tools.iter().enumerate() {
1190                    let state = if index == self.curr_editor_tool {
1191                        TheWidgetState::Selected
1192                    } else {
1193                        TheWidgetState::None
1194                    };
1195                    ctx.ui.set_widget_state(tool.id().name.clone(), state);
1196                }
1197
1198                self.editor_tools[old_tool_index].deactivate();
1199                self.editor_tools[self.curr_editor_tool].activate();
1200                self.apply_editor_rgba_mode(ui, ctx);
1201            }
1202        } else {
1203            // Handle game tool switching
1204            for (index, tool) in self.game_tools.iter().enumerate() {
1205                if tool.id().uuid == tool_id && index != self.curr_game_tool {
1206                    switched_tool = true;
1207                    old_tool_index = self.curr_game_tool;
1208                    self.curr_game_tool = index;
1209                    redraw = true;
1210                }
1211            }
1212            if switched_tool {
1213                for tool in self.game_tools.iter() {
1214                    if tool.id().uuid != tool_id {
1215                        ctx.ui
1216                            .set_widget_state(tool.id().name.clone(), TheWidgetState::None);
1217                    }
1218                }
1219                self.game_tools[old_tool_index].tool_event(
1220                    ToolEvent::DeActivate,
1221                    ui,
1222                    ctx,
1223                    project,
1224                    server_ctx,
1225                );
1226
1227                // Switching game tools should collapse any maximized dock/editor view.
1228                DOCKMANAGER.write().unwrap().minimize(ui, ctx);
1229            }
1230
1231            if let Some(layout) = ui.get_hlayout(layout_name) {
1232                layout.clear();
1233                layout.set_reverse_index(None);
1234                ctx.ui.redraw_all = true;
1235            }
1236
1237            self.get_current_tool()
1238                .tool_event(ToolEvent::Activate, ui, ctx, project, server_ctx);
1239
1240            self.update_geometry_overlay_3d(project, server_ctx);
1241
1242            crate::editor::RUSTERIX.write().unwrap().set_dirty();
1243        }
1244
1245        /*
1246        if let Some(layout) = ui.get_hlayout(layout_name) {
1247            if layout.widgets().is_empty() {
1248                // Add default widgets
1249
1250                // let mut gb = TheGroupButton::new(TheId::named("2D3D Group"));
1251                // gb.add_text("2D Map".to_string());
1252                // gb.add_text("Mixed".to_string());
1253                // gb.add_text("3D Map".to_string());
1254
1255                // match *RENDERMODE.lock().unwrap() {
1256                //     EditorDrawMode::Draw2D => gb.set_index(0),
1257                //     EditorDrawMode::DrawMixed => gb.set_index(1),
1258                //     EditorDrawMode::Draw3D => gb.set_index(2),
1259                // }
1260
1261                // let mut time_slider = TheTimeSlider::new(TheId::named("Server Time Slider"));
1262                // time_slider.set_continuous(true);
1263                // time_slider.limiter_mut().set_max_width(400);
1264                // time_slider.set_value(TheValue::Time(self.server_time));
1265
1266                let mut spacer = TheSpacer::new(TheId::empty());
1267                spacer.limiter_mut().set_max_width(30);
1268
1269                let mut render_button = TheTraybarButton::new(TheId::named("Render Button"));
1270                render_button.set_text(self.render_button_text.clone());
1271                render_button.set_status_text("Controls the 3D background renderer. During rendering it displays how many tiles are left to render.");
1272                render_button.set_fixed_size(true);
1273                render_button.limiter_mut().set_max_width(80);
1274
1275                render_button.set_context_menu(Some(TheContextMenu {
1276                    items: vec![
1277                        TheContextMenuItem::new(
1278                            "Start Renderer".to_string(),
1279                            TheId::named("Start Renderer"),
1280                        ),
1281                        TheContextMenuItem::new(
1282                            "Pause".to_string(),
1283                            TheId::named("Pause Renderer"),
1284                        ),
1285                        TheContextMenuItem::new(
1286                            "Restart".to_string(),
1287                            TheId::named("Restart Renderer"),
1288                        ),
1289                    ],
1290                    ..Default::default()
1291                }));
1292
1293                //layout.add_widget(Box::new(gb));
1294                layout.add_widget(Box::new(spacer));
1295                //layout.add_widget(Box::new(time_slider));
1296                layout.add_widget(Box::new(render_button));
1297                layout.set_reverse_index(Some(1));
1298            }
1299        }*/
1300
1301        ctx.ui.relayout = true;
1302
1303        redraw
1304    }
1305
1306    // Return the uuid given game tool.
1307    pub fn get_game_tool_uuid_of_name(&self, name: &str) -> Option<Uuid> {
1308        for tool in self.game_tools.iter() {
1309            if tool.id().name == name {
1310                return Some(tool.id().uuid);
1311            }
1312        }
1313        None
1314    }
1315
1316    // Return the tool of the given name
1317    pub fn get_game_tool_of_name(&mut self, name: &str) -> Option<&mut Box<dyn Tool>> {
1318        for tool in self.game_tools.iter_mut() {
1319            if tool.id().name == name {
1320                return Some(tool);
1321            }
1322        }
1323        None
1324    }
1325
1326    /// Update the overlay geometry.
1327    pub fn update_geometry_overlay_3d(
1328        &mut self,
1329        project: &mut Project,
1330        server_ctx: &mut ServerContext,
1331    ) {
1332        if server_ctx.editor_view_mode == EditorViewMode::D2 {
1333            return;
1334        }
1335
1336        let mut rusterix = RUSTERIX.write().unwrap();
1337        rusterix.scene_handler.clear_overlay();
1338        // rusterix.scene_handler.vm.set_layer_activity_logging(true);
1339
1340        // basis_vectors returns (forward, right, up)
1341        let (cam_forward, cam_right, cam_up) = rusterix.client.camera_d3.basis_vectors();
1342        let view_right = cam_right;
1343        let view_up = cam_up;
1344        let view_nudge = cam_forward * -0.002; // small toward-camera nudge to avoid z-fighting
1345        rusterix.client.scene.d3_overlay.clear();
1346        let thickness = 0.15;
1347
1348        if let Some(region) = project.get_region_ctx(&server_ctx) {
1349            let map = &region.map;
1350
1351            // Helper to draw a single world-space line into the overlay
1352            let push_line = |id: GeoId,
1353                             rusterix: &mut rusterix::Rusterix,
1354                             mut a: Vec3<f32>,
1355                             mut b: Vec3<f32>,
1356                             normal: Vec3<f32>,
1357                             selected: bool,
1358                             hovered: bool| {
1359                // Z-fight mitigation: nudge along CAMERA FORWARD, not the line normal
1360                if selected {
1361                    let extra_nudge = cam_forward * -0.004; // toward camera
1362                    a += extra_nudge;
1363                    b += extra_nudge;
1364                }
1365
1366                let tile_id = if selected || hovered {
1367                    rusterix.scene_handler.selected
1368                } else {
1369                    rusterix.scene_handler.white
1370                };
1371
1372                rusterix
1373                    .scene_handler
1374                    .overlay_3d
1375                    .add_line_3d(id, tile_id, a, b, thickness, normal, 100);
1376            };
1377
1378            // Rect tool previews
1379
1380            if let Some((top_left, bottom_right)) = map.curr_rectangle {
1381                let mut index = 0;
1382                let min = Vec2::new(
1383                    top_left.x.min(bottom_right.x),
1384                    top_left.y.min(bottom_right.y),
1385                );
1386                let max = Vec2::new(
1387                    top_left.x.max(bottom_right.x),
1388                    bottom_right.y.max(top_left.y),
1389                );
1390
1391                let corners = [
1392                    Vec2::new(min.x, min.y),
1393                    Vec2::new(max.x, min.y),
1394                    Vec2::new(max.x, max.y),
1395                    Vec2::new(min.x, max.y),
1396                ];
1397                let color = rusterix.scene_handler.white;
1398
1399                // Draw 4 edges (close the loop by wrapping 3→0) in 2D overlay
1400                for i in 0..4 {
1401                    let a = corners[i];
1402                    let b = corners[(i + 1) % 4];
1403                    rusterix.scene_handler.add_overlay_2d_line(
1404                        GeoId::Gizmo(index),
1405                        a,
1406                        b,
1407                        color,
1408                        100,
1409                    );
1410                    index += 1;
1411                }
1412            }
1413
1414            if server_ctx.curr_map_tool_type == MapToolType::Rect {
1415                if let Some(terrain_id) = server_ctx.rect_terrain_id {
1416                    let mut index = 0;
1417                    let config = TerrainConfig::default();
1418                    let corners = TerrainGenerator::tile_outline_world(map, terrain_id, &config);
1419                    let n = TerrainGenerator::tile_normal(map, terrain_id, &config);
1420
1421                    // Draw 4 edges (close the loop by wrapping 3→0)
1422                    for i in 0..4 {
1423                        let a = corners[i] + view_nudge;
1424                        let b = corners[(i + 1) % 4] + view_nudge;
1425                        push_line(GeoId::Unknown(index), &mut rusterix, a, b, n, false, false);
1426                        index += 1;
1427                    }
1428                } else if let Some(sector_id) = server_ctx.rect_sector_id_3d {
1429                    let mut index = 0;
1430                    for (_, surface) in &map.surfaces {
1431                        if surface.sector_id == sector_id {
1432                            let corners =
1433                                surface.tile_outline_world_local(server_ctx.rect_tile_id_3d, map);
1434                            let n = surface.plane.normal;
1435
1436                            // Draw 4 edges (close the loop by wrapping 3→0)
1437                            for i in 0..4 {
1438                                let a = corners[i] + view_nudge;
1439                                let b = corners[(i + 1) % 4] + view_nudge;
1440                                push_line(
1441                                    GeoId::Unknown(index),
1442                                    &mut rusterix,
1443                                    a,
1444                                    b,
1445                                    n,
1446                                    false,
1447                                    false,
1448                                );
1449                                index += 1;
1450                            }
1451                        }
1452                    }
1453                }
1454            }
1455
1456            if !server_ctx.show_editing_geometry {
1457                rusterix.scene_handler.set_overlay();
1458                return;
1459            }
1460
1461            // Helper to draw a single vertex as a camera-facing billboard in the overlay
1462            let vertex_size_world = 0.24_f32; // slightly larger for visibility
1463            let push_vertex =
1464                |id: GeoId, p: Vec3<f32>, selected: bool, rusterix: &mut rusterix::Rusterix| {
1465                    let tile_id = if selected {
1466                        rusterix.scene_handler.selected
1467                    } else {
1468                        rusterix.scene_handler.white
1469                    };
1470                    rusterix.scene_handler.overlay_3d.add_billboard_3d(
1471                        id,
1472                        tile_id,
1473                        p,
1474                        view_right,
1475                        view_up,
1476                        vertex_size_world,
1477                        true,
1478                    );
1479                };
1480
1481            if server_ctx.curr_map_tool_type == MapToolType::Vertex {
1482                for v in map.vertices.iter() {
1483                    let Some(world_pos) = map.get_vertex_3d(v.id) else {
1484                        continue;
1485                    };
1486                    let mut pos = Vec3::new(world_pos.x, world_pos.y, world_pos.z);
1487                    pos += view_nudge;
1488                    let selected =
1489                        map.selected_vertices.contains(&v.id) || server_ctx.hover.0 == Some(v.id);
1490
1491                    push_vertex(GeoId::Vertex(v.id), pos, selected, &mut rusterix);
1492                }
1493            } else {
1494                // Linedefs
1495                if server_ctx.curr_map_tool_type == MapToolType::Linedef {
1496                    for linedef in &map.linedefs {
1497                        if linedef.sector_ids.is_empty() {
1498                            if let (Some(vs), Some(ve)) = (
1499                                map.get_vertex_3d(linedef.start_vertex),
1500                                map.get_vertex_3d(linedef.end_vertex),
1501                            ) {
1502                                let a = Vec3::new(vs.x, vs.y, vs.z) + view_nudge;
1503                                let b = Vec3::new(ve.x, ve.y, ve.z) + view_nudge;
1504                                let normal = cam_forward;
1505
1506                                let is_selected = map.selected_linedefs.contains(&linedef.id);
1507                                let is_hovered = server_ctx.hover.1 == Some(linedef.id);
1508
1509                                push_line(
1510                                    GeoId::Linedef(linedef.id),
1511                                    &mut rusterix,
1512                                    a,
1513                                    b,
1514                                    normal,
1515                                    is_selected,
1516                                    is_hovered,
1517                                );
1518                            }
1519                        }
1520                    }
1521                }
1522
1523                // Sectors
1524                use std::collections::HashMap;
1525                #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
1526                struct EdgeKey {
1527                    v0: u32,
1528                    v1: u32,
1529                }
1530                impl EdgeKey {
1531                    fn from_vertices(a: u32, b: u32) -> Self {
1532                        if a < b {
1533                            EdgeKey { v0: a, v1: b }
1534                        } else {
1535                            EdgeKey { v0: b, v1: a }
1536                        }
1537                    }
1538                }
1539                #[derive(Clone)]
1540                struct EdgeInfo {
1541                    a: Vec3<f32>,
1542                    b: Vec3<f32>,
1543                    selected: bool,
1544                    hovered: bool,
1545                    rep_ld_id: u32, // representative linedef id for picking/hit-testing
1546                }
1547                let mut edge_accum: HashMap<EdgeKey, EdgeInfo> = HashMap::new();
1548
1549                for surface in map.surfaces.values() {
1550                    let sector_id = surface.sector_id;
1551                    let Some(sector) = map.find_sector(sector_id) else {
1552                        continue;
1553                    };
1554                    let sector_is_selected = map.selected_sectors.contains(&sector_id);
1555
1556                    if sector.properties.contains("rect") && server_ctx.no_rect_geo_on_map {
1557                        continue;
1558                    }
1559
1560                    let nudge = view_nudge; // consistent camera-side nudge avoids opposite-face z-fight
1561
1562                    if let Some(points3) = sector.vertices_world(map) {
1563                        let n_pts = points3.len();
1564                        let n_ld = sector.linedefs.len();
1565                        let n = n_pts.min(n_ld);
1566                        if n >= 2 {
1567                            for i in 0..n {
1568                                let a = points3[i] + nudge;
1569                                let b = points3[(i + 1) % n_pts] + nudge;
1570                                let ld_id = sector.linedefs[i];
1571
1572                                let mut line_is_selected = false;
1573
1574                                if server_ctx.curr_map_tool_type == MapToolType::Linedef
1575                                    || server_ctx.curr_map_tool_type == MapToolType::Selection
1576                                {
1577                                    line_is_selected = map.selected_linedefs.contains(&ld_id)
1578                                        || server_ctx.hover.1 == Some(ld_id);
1579                                } else if server_ctx.curr_map_tool_type == MapToolType::Sector {
1580                                    line_is_selected =
1581                                        sector_is_selected || server_ctx.hover.2 == Some(sector_id);
1582                                };
1583
1584                                // Build unordered edge key from linedef vertices, fallback if not found
1585                                let key = if let Some(ld_ref) = map.find_linedef(ld_id) {
1586                                    EdgeKey::from_vertices(ld_ref.start_vertex, ld_ref.end_vertex)
1587                                } else {
1588                                    // Fallback: build a key from the nearest map vertices to a/b (should be rare)
1589                                    continue;
1590                                };
1591
1592                                edge_accum
1593                                    .entry(key)
1594                                    .and_modify(|e| {
1595                                        e.selected |= line_is_selected;
1596                                        e.hovered |= server_ctx.hover.1 == Some(ld_id);
1597                                        e.a = a;
1598                                        e.b = b; // keep latest endpoints
1599                                    })
1600                                    .or_insert(EdgeInfo {
1601                                        a,
1602                                        b,
1603                                        selected: line_is_selected,
1604                                        hovered: server_ctx.hover.1 == Some(ld_id),
1605                                        rep_ld_id: ld_id,
1606                                    });
1607                            }
1608                        }
1609                    }
1610                }
1611
1612                // Emit deduplicated edges
1613                for (_key, e) in edge_accum.into_iter() {
1614                    push_line(
1615                        // &mut overlay_batches,
1616                        // GeometrySource::Linedef(e.rep_ld_id),
1617                        GeoId::Linedef(e.rep_ld_id),
1618                        &mut rusterix,
1619                        e.a,
1620                        e.b,
1621                        cam_forward,
1622                        e.selected,
1623                        e.hovered,
1624                    );
1625                }
1626            }
1627
1628            // Flush final overlay batches: draw normal overlays first, then highlighted front overlays last
1629            // for batch in overlay_batches.drain(..) {
1630            //     rusterix.client.scene.d3_overlay.push(batch);
1631            // }
1632            // for batch in overlay_batches_front.drain(..) {
1633            //     rusterix.client.scene.d3_overlay.push(batch);
1634            // }
1635        }
1636
1637        rusterix.scene_handler.set_overlay();
1638    }
1639    /*
1640    pub fn hitpoint_to_editing_coord(
1641        &mut self,
1642        project: &mut Project,
1643        server_ctx: &mut ServerContext,
1644        hp: Vec3<f32>,
1645    ) -> Option<Vec2<f32>> {
1646        let mut rc = None;
1647
1648        let mut rusterix = RUSTERIX.write().unwrap();
1649        rusterix.client.scene.d3_overlay.clear();
1650
1651        if let Some(region) = project.get_region_ctx(&server_ctx) {
1652            // Meta provides world-space normal and the span (region 2D) for wall profiles
1653            //let (_, span) = server_ctx.get_region_map_meta_data(region);
1654
1655            if span.is_none() {
1656                rc = Some(Vec2::new(hp.x, hp.z));
1657            } else {
1658                // PROFILE MAP: convert world hitpoint to (x,y) in profile space
1659                // 1) Find owning linedef
1660                let mut owner_linedef_opt = None;
1661                for ld in &region.map.linedefs {
1662                    if Some(ld.id) == server_ctx.profile_view {
1663                        owner_linedef_opt = Some(ld);
1664                        break;
1665                    }
1666                }
1667                if owner_linedef_opt.is_none() {
1668                    return rc;
1669                }
1670                let linedef = owner_linedef_opt.unwrap();
1671
1672                // 2) Span basis
1673                let (p0, p1) = span.unwrap();
1674                let delta = p1 - p0;
1675                let len = delta.magnitude();
1676                if len <= 1e-6 {
1677                    return rc;
1678                }
1679                let dir = delta / len; // along wall (world XZ)
1680
1681                // 3) Base elevation from front sector (default 0.0)
1682                let base_elevation = if let Some(front_id) = linedef.front_sector {
1683                    if let Some(front) = region.map.sectors.get(front_id as usize) {
1684                        front.properties.get_float_default("floor_height", 0.0)
1685                    } else {
1686                        0.0
1687                    }
1688                } else {
1689                    0.0
1690                };
1691
1692                // 4) Inward offset used during placement; subtract before projecting
1693                let inward = Vec2::new(-dir.y, dir.x);
1694                let eps = linedef
1695                    .properties
1696                    .get_float("profile_wall_epsilon")
1697                    .unwrap_or(0.001);
1698                let offset2 = if linedef.front_sector.is_some() {
1699                    inward * eps
1700                } else if linedef.back_sector.is_some() {
1701                    inward * -eps
1702                } else {
1703                    Vec2::new(0.0, 0.0)
1704                };
1705
1706                // 5) Determine profile left/right anchors
1707                let profile = &linedef.profile;
1708                let mut left_x = f32::INFINITY;
1709                let mut right_x = f32::NEG_INFINITY;
1710                for v in &profile.vertices {
1711                    if let Some(id) = v.properties.get_int("profile_id") {
1712                        match id {
1713                            1 | 2 => left_x = left_x.min(v.x),
1714                            0 | 3 => right_x = right_x.max(v.x),
1715                            _ => {}
1716                        }
1717                    }
1718                }
1719                if !left_x.is_finite() || !right_x.is_finite() {
1720                    left_x = f32::INFINITY;
1721                    right_x = f32::NEG_INFINITY;
1722                    for v in &profile.vertices {
1723                        left_x = left_x.min(v.x);
1724                        right_x = right_x.max(v.x);
1725                    }
1726                }
1727                let width = (right_x - left_x).max(1e-6);
1728
1729                // 6) Project hitpoint onto span to get t in [0,1]
1730                let pos2 = Vec2::new(hp.x, hp.z) - offset2; // undo inward offset
1731                let t = ((pos2 - p0).dot(dir) / len).clamp(0.0, 1.0);
1732                let x2d = left_x + t * width;
1733
1734                // 7) Y in profile space
1735                let y2d = hp.y - base_elevation;
1736
1737                rc = Some(Vec2::new(x2d, y2d));
1738            }
1739        }
1740
1741        rc
1742    }*/
1743
1744    /// Get the geometry hit at the given screen position.
1745    fn get_geometry_hit(
1746        &self,
1747        render_view: &dyn TheRenderViewTrait,
1748        coord: Vec2<i32>,
1749        server_ctx: &mut ServerContext,
1750    ) -> Option<(GeoId, Vec3<f32>)> {
1751        let dim = *render_view.dim();
1752
1753        let screen_uv = [
1754            coord.x as f32 / dim.width as f32,
1755            coord.y as f32 / dim.height as f32,
1756        ];
1757
1758        let mut rusterix = RUSTERIX.write().unwrap();
1759
1760        server_ctx.hover_cursor_3d = None;
1761        if let Some(raw) = rusterix.scene_handler.vm.pick_geo_id_at_uv(
1762            dim.width as u32,
1763            dim.height as u32,
1764            screen_uv,
1765            false,
1766            false,
1767        ) {
1768            server_ctx.hover_cursor_3d = Some(raw.1);
1769            if server_ctx.curr_map_tool_type == MapToolType::Sector {
1770                return Some((raw.0, raw.1));
1771            }
1772        }
1773
1774        if server_ctx.curr_map_tool_type != MapToolType::Sector {
1775            rusterix.scene_handler.vm.set_active_vm(2);
1776        }
1777
1778        let rc = rusterix.scene_handler.vm.pick_geo_id_at_uv(
1779            dim.width as u32,
1780            dim.height as u32,
1781            screen_uv,
1782            false,
1783            false,
1784        );
1785
1786        rusterix.scene_handler.vm.set_active_vm(0);
1787
1788        rc.map(|(geo_id, pos, _)| (geo_id, pos))
1789    }
1790}