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 = ®ion.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(§or_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 ®ion.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}