rustapi/tools/sector.rs
1use crate::actions::edit_sector::EDIT_SECTOR_ACTION_ID;
2use crate::editor::RUSTERIX;
3use crate::hud::{Hud, HudMode};
4use crate::prelude::*;
5use MapEvent::*;
6use ToolEvent::*;
7use rusterix::{Assets, PixelSource, Value};
8use scenevm::GeoId;
9use std::str::FromStr;
10use vek::Vec2;
11
12pub struct SectorTool {
13 id: TheId,
14 click_pos: Vec2<f32>,
15 click_pos_3d: Vec3<f32>,
16 /// The initial ray intersection point on the drag plane at click time
17 click_ray_intersection_3d: Option<Vec3<f32>>,
18 rectangle_undo_map: Map,
19 click_selected: bool,
20 drag_changed: bool,
21 was_clicked: bool,
22 vertices_duplicated: bool,
23 cached_sectors_to_move: Vec<u32>,
24
25 hud: Hud,
26}
27
28impl Tool for SectorTool {
29 fn new() -> Self
30 where
31 Self: Sized,
32 {
33 Self {
34 id: TheId::named("Sector Tool"),
35 click_pos: Vec2::zero(),
36 click_pos_3d: Vec3::zero(),
37 click_ray_intersection_3d: None,
38 click_selected: false,
39 drag_changed: false,
40 rectangle_undo_map: Map::default(),
41 was_clicked: false,
42 vertices_duplicated: false,
43 cached_sectors_to_move: vec![],
44
45 hud: Hud::new(HudMode::Sector),
46 }
47 }
48
49 fn id(&self) -> TheId {
50 self.id.clone()
51 }
52 fn info(&self) -> String {
53 fl!("tool_sector")
54 }
55 fn icon_name(&self) -> String {
56 str!("polygon")
57 }
58 fn accel(&self) -> Option<char> {
59 Some('E')
60 }
61
62 fn help_url(&self) -> Option<String> {
63 Some("docs/creator/tools/sector".to_string())
64 }
65
66 fn tool_event(
67 &mut self,
68 tool_event: ToolEvent,
69 ui: &mut TheUI,
70 ctx: &mut TheContext,
71 project: &mut Project,
72 server_ctx: &mut ServerContext,
73 ) -> bool {
74 match tool_event {
75 Activate => {
76 // Display the tile edit panel.
77 ctx.ui
78 .send(TheEvent::SetStackIndex(TheId::named("Main Stack"), 0));
79
80 if let Some(layout) = ui.get_sharedhlayout("Shared Panel Layout") {
81 layout.set_mode(TheSharedHLayoutMode::Right);
82 ctx.ui.relayout = true;
83 }
84
85 server_ctx.curr_map_tool_type = MapToolType::Sector;
86
87 if let Some(map) = project.get_map_mut(server_ctx) {
88 map.selected_vertices.clear();
89 map.selected_linedefs.clear();
90 }
91
92 ctx.ui.send(TheEvent::Custom(
93 TheId::named("Map Selection Changed"),
94 TheValue::Empty,
95 ));
96
97 return true;
98 }
99 _ => {
100 server_ctx.curr_map_tool_type = MapToolType::General;
101 }
102 };
103 false
104 }
105
106 fn map_event(
107 &mut self,
108 map_event: MapEvent,
109 ui: &mut TheUI,
110 ctx: &mut TheContext,
111 map: &mut Map,
112 server_ctx: &mut ServerContext,
113 ) -> Option<ProjectUndoAtom> {
114 let mut undo_atom: Option<ProjectUndoAtom> = None;
115
116 match map_event {
117 MapKey(c) => {
118 match c {
119 '1'..='9' => map.subdivisions = (c as u8 - b'0') as f32,
120 '0' => map.subdivisions = 10.0,
121 _ => {}
122 }
123 crate::editor::RUSTERIX.write().unwrap().set_dirty();
124 }
125 MapClicked(coord) => {
126 if self.hud.clicked(coord.x, coord.y, map, ui, ctx, server_ctx) {
127 self.was_clicked = false;
128 crate::editor::RUSTERIX.write().unwrap().set_dirty();
129 return None;
130 }
131 self.was_clicked = true;
132 let mut changed = false;
133
134 if server_ctx.editor_view_mode != EditorViewMode::D2 && server_ctx.hover.2.is_none()
135 {
136 let hit_pos = server_ctx.hover_cursor_3d.unwrap_or(server_ctx.geo_hit_pos);
137 server_ctx.hover.2 = map
138 .find_sector_at(Vec2::new(hit_pos.x, hit_pos.z))
139 .map(|s| s.id);
140 }
141
142 self.click_selected = false;
143 if server_ctx.hover.2.is_some() {
144 map.selected_entity_item = None;
145
146 if ui.shift {
147 // Add
148 if let Some(s) = server_ctx.hover.2 {
149 if !map.selected_sectors.contains(&s) {
150 map.selected_sectors.push(s);
151 changed = true;
152 }
153 self.click_selected = true;
154 }
155 } else if ui.alt {
156 // Subtract
157 if let Some(v) = server_ctx.hover.2 {
158 map.selected_sectors.retain(|&selected| selected != v);
159 changed = true;
160 }
161 } else {
162 // Replace
163 if let Some(v) = server_ctx.hover.2 {
164 map.selected_sectors = vec![v];
165 changed = true;
166 } else {
167 map.selected_sectors.clear();
168 changed = true;
169 }
170 self.click_selected = true;
171 }
172
173 if changed {
174 server_ctx.curr_action_id =
175 Some(Uuid::from_str(EDIT_SECTOR_ACTION_ID).unwrap());
176 ctx.ui.send(TheEvent::Custom(
177 TheId::named("Map Selection Changed"),
178 TheValue::Empty,
179 ));
180 }
181 }
182
183 self.click_pos = Vec2::new(coord.x as f32, coord.y as f32);
184 self.click_ray_intersection_3d = None;
185
186 // For 3D dragging, use the average position of selected sector vertices
187 if self.click_selected && !map.selected_sectors.is_empty() {
188 let mut sum_pos = Vec3::zero();
189 let mut count = 0;
190 for sector_id in &map.selected_sectors {
191 if let Some(sector) = map.find_sector(*sector_id) {
192 for line_id in §or.linedefs {
193 if let Some(line) = map.find_linedef(*line_id) {
194 if let Some(v1) = map.find_vertex(line.start_vertex) {
195 sum_pos += v1.as_vec3_world();
196 count += 1;
197 }
198 if let Some(v2) = map.find_vertex(line.end_vertex) {
199 sum_pos += v2.as_vec3_world();
200 count += 1;
201 }
202 }
203 }
204 }
205 }
206 if count > 0 {
207 self.click_pos_3d = sum_pos / count as f32;
208 } else {
209 self.click_pos_3d = server_ctx.geo_hit_pos;
210 }
211
212 // Compute initial ray intersection on the drag plane at click time
213 if server_ctx.editor_view_mode != EditorViewMode::D2 {
214 if let Some(render_view) = ui.get_render_view("PolyView") {
215 let dim = *render_view.dim();
216 let screen_uv = [
217 coord.x as f32 / dim.width as f32,
218 coord.y as f32 / dim.height as f32,
219 ];
220
221 let rusterix = RUSTERIX.read().unwrap();
222 let ray = rusterix.client.camera_d3.create_ray(
223 Vec2::new(screen_uv[0], 1.0 - screen_uv[1]),
224 Vec2::new(dim.width as f32, dim.height as f32),
225 Vec2::zero(),
226 );
227 drop(rusterix);
228
229 let plane = server_ctx.gizmo_mode;
230 let plane_normal = match plane {
231 GizmoMode::XZ => Vec3::new(0.0, 1.0, 0.0),
232 GizmoMode::XY => Vec3::new(0.0, 0.0, 1.0),
233 GizmoMode::YZ => Vec3::new(1.0, 0.0, 0.0),
234 };
235
236 let denom: f32 = plane_normal.dot(ray.dir);
237 if denom.abs() > 0.0001 {
238 let t = (self.click_pos_3d - ray.origin).dot(plane_normal) / denom;
239 if t >= 0.0 {
240 self.click_ray_intersection_3d = Some(ray.origin + ray.dir * t);
241 }
242 }
243 }
244 }
245 } else {
246 self.click_pos_3d = server_ctx.geo_hit_pos;
247 }
248
249 self.rectangle_undo_map = map.clone();
250 self.vertices_duplicated = false;
251 self.cached_sectors_to_move.clear();
252 }
253 MapDragged(coord) => {
254 if self.hud.dragged(coord.x, coord.y, map, ui, ctx, server_ctx) {
255 crate::editor::RUSTERIX.write().unwrap().set_dirty();
256 return None;
257 }
258
259 if self.click_selected {
260 // Dragging selected sectors
261 if let Some(render_view) = ui.get_render_view("PolyView") {
262 let dim = *render_view.dim();
263
264 if server_ctx.editor_view_mode == EditorViewMode::D2 {
265 // 2D dragging
266 let click_pos = server_ctx.local_to_map_grid(
267 Vec2::new(dim.width as f32, dim.height as f32),
268 self.click_pos,
269 map,
270 map.subdivisions,
271 );
272 let drag_pos = server_ctx.local_to_map_grid(
273 Vec2::new(dim.width as f32, dim.height as f32),
274 Vec2::new(coord.x as f32, coord.y as f32),
275 map,
276 map.subdivisions,
277 );
278
279 let mut selected_vertices = vec![];
280
281 let drag_delta = click_pos - drag_pos;
282
283 // Collect sectors to move (selected + optionally embedded)
284 // Only compute this once and cache it
285 if self.cached_sectors_to_move.is_empty() {
286 self.cached_sectors_to_move =
287 self.rectangle_undo_map.selected_sectors.clone();
288
289 // If Ctrl is pressed, include all embedded sectors
290 if ui.ctrl {
291 let mut embedded_sectors = vec![];
292 for sector_id in &self.rectangle_undo_map.selected_sectors {
293 let embedded = map.find_embedded_sectors(*sector_id);
294 for emb_id in embedded {
295 if !self.cached_sectors_to_move.contains(&emb_id) {
296 embedded_sectors.push(emb_id);
297 }
298 }
299 }
300 self.cached_sectors_to_move.extend(embedded_sectors);
301 }
302 }
303
304 let sectors_to_move = &self.cached_sectors_to_move;
305
306 for sector_id in sectors_to_move.iter() {
307 if let Some(sector) =
308 self.rectangle_undo_map.find_sector(*sector_id)
309 {
310 for line_id in §or.linedefs {
311 if let Some(line) =
312 self.rectangle_undo_map.find_linedef(*line_id)
313 {
314 selected_vertices.push(line.start_vertex);
315 selected_vertices.push(line.end_vertex);
316 }
317 }
318 }
319 }
320
321 // Duplicate shared vertices only once at the start of dragging
322 if !self.vertices_duplicated {
323 for vertex_id in selected_vertices.iter() {
324 // Check if this vertex is shared with any unselected rect sector
325 let is_unselected_rect_vertex =
326 map.sectors.iter().any(|sector| {
327 if sector.properties.contains("rect")
328 && !sectors_to_move.contains(§or.id)
329 {
330 sector.linedefs.iter().any(|&line_id| {
331 if let Some(line) = map.find_linedef(line_id) {
332 line.start_vertex == *vertex_id
333 || line.end_vertex == *vertex_id
334 } else {
335 false
336 }
337 })
338 } else {
339 false
340 }
341 });
342
343 if is_unselected_rect_vertex {
344 // Vertex is shared with rect geometry - duplicate it for the sectors being moved
345 if let Some(new_vertex_id) =
346 map.duplicate_vertex(*vertex_id)
347 {
348 // Replace old vertex with new vertex in all sectors being moved
349 for sector_id in sectors_to_move.iter() {
350 map.replace_vertex_in_sector(
351 *sector_id,
352 *vertex_id,
353 new_vertex_id,
354 );
355 }
356 }
357 }
358 }
359 self.vertices_duplicated = true;
360 // Update rectangle_undo_map after duplication so future drags use correct vertex IDs
361 self.rectangle_undo_map = map.clone();
362 }
363
364 // Re-collect vertices from sectors (they may have new IDs after duplication)
365 let mut current_vertices = vec![];
366 for sector_id in sectors_to_move.iter() {
367 if let Some(sector) = map.find_sector(*sector_id) {
368 for line_id in §or.linedefs {
369 if let Some(line) = map.find_linedef(*line_id) {
370 if !current_vertices.contains(&line.start_vertex) {
371 current_vertices.push(line.start_vertex);
372 }
373 if !current_vertices.contains(&line.end_vertex) {
374 current_vertices.push(line.end_vertex);
375 }
376 }
377 }
378 }
379 }
380
381 for vertex_id in current_vertices.iter() {
382 if let Some(original_vertex) =
383 self.rectangle_undo_map.find_vertex(*vertex_id)
384 {
385 let new_pos = Vec2::new(
386 original_vertex.x - drag_delta.x,
387 original_vertex.y - drag_delta.y,
388 );
389 map.update_vertex(*vertex_id, new_pos);
390 }
391 }
392 server_ctx.hover_cursor = Some(drag_pos);
393
394 if drag_delta.x != 0.0 || drag_delta.y != 0.0 {
395 self.drag_changed = true;
396 }
397 } else {
398 // 3D dragging
399 // Only start dragging after a minimum distance threshold
400 let drag_distance = self
401 .click_pos
402 .distance(Vec2::new(coord.x as f32, coord.y as f32));
403 if drag_distance < 5.0 {
404 crate::editor::RUSTERIX.write().unwrap().set_dirty();
405 return None;
406 }
407
408 // Use the initial ray intersection as reference (not vertex average)
409 // This prevents the "jump" when starting to drag
410 let click_intersection = match self.click_ray_intersection_3d {
411 Some(pos) => pos,
412 None => {
413 crate::editor::RUSTERIX.write().unwrap().set_dirty();
414 return None;
415 }
416 };
417
418 let start_pos = self.click_pos_3d;
419 let plane = server_ctx.gizmo_mode;
420
421 let screen_uv = [
422 coord.x as f32 / dim.width as f32,
423 coord.y as f32 / dim.height as f32,
424 ];
425
426 let rusterix = RUSTERIX.read().unwrap();
427 let ray = rusterix.client.camera_d3.create_ray(
428 Vec2::new(screen_uv[0], 1.0 - screen_uv[1]),
429 Vec2::new(dim.width as f32, dim.height as f32),
430 Vec2::zero(),
431 );
432 drop(rusterix);
433
434 let plane_normal = match plane {
435 GizmoMode::XZ => Vec3::new(0.0, 1.0, 0.0),
436 GizmoMode::XY => Vec3::new(0.0, 0.0, 1.0),
437 GizmoMode::YZ => Vec3::new(1.0, 0.0, 0.0),
438 };
439
440 let denom: f32 = plane_normal.dot(ray.dir);
441 if denom.abs() > 0.0001 {
442 let t = (start_pos - ray.origin).dot(plane_normal) / denom;
443
444 if t >= 0.0 {
445 let current_pos = ray.origin + ray.dir * t;
446
447 // Calculate drag delta relative to initial click intersection
448 // (not the vertex average position)
449 let drag_delta = match plane {
450 GizmoMode::XZ => {
451 // XZ plane: allow movement in X and Z, lock Y
452 Vec3::new(
453 current_pos.x - click_intersection.x,
454 0.0,
455 current_pos.z - click_intersection.z,
456 )
457 }
458 GizmoMode::XY => {
459 // XY plane: allow movement in X and Y, lock Z
460 Vec3::new(
461 current_pos.x - click_intersection.x,
462 current_pos.y - click_intersection.y,
463 0.0,
464 )
465 }
466 GizmoMode::YZ => {
467 // YZ plane: allow movement in Y and Z, lock X
468 Vec3::new(
469 0.0,
470 current_pos.y - click_intersection.y,
471 current_pos.z - click_intersection.z,
472 )
473 }
474 };
475
476 // Only compute sectors to move once and cache it
477 if self.cached_sectors_to_move.is_empty() {
478 self.cached_sectors_to_move =
479 self.rectangle_undo_map.selected_sectors.clone();
480
481 // If Ctrl is pressed, include all embedded sectors
482 if ui.ctrl {
483 let mut embedded_sectors = vec![];
484 for sector_id in
485 &self.rectangle_undo_map.selected_sectors
486 {
487 let embedded =
488 map.find_embedded_sectors(*sector_id);
489 for emb_id in embedded {
490 if !self
491 .cached_sectors_to_move
492 .contains(&emb_id)
493 {
494 embedded_sectors.push(emb_id);
495 }
496 }
497 }
498 self.cached_sectors_to_move.extend(embedded_sectors);
499 }
500 }
501
502 let sectors_to_move = &self.cached_sectors_to_move;
503
504 let mut selected_vertices = vec![];
505 for sector_id in sectors_to_move.iter() {
506 if let Some(sector) =
507 self.rectangle_undo_map.find_sector(*sector_id)
508 {
509 for line_id in §or.linedefs {
510 if let Some(line) =
511 self.rectangle_undo_map.find_linedef(*line_id)
512 {
513 if !selected_vertices
514 .contains(&line.start_vertex)
515 {
516 selected_vertices.push(line.start_vertex);
517 }
518 if !selected_vertices.contains(&line.end_vertex)
519 {
520 selected_vertices.push(line.end_vertex);
521 }
522 }
523 }
524 }
525 }
526
527 // Duplicate shared vertices only once at the start of dragging
528 if !self.vertices_duplicated {
529 for vertex_id in selected_vertices.iter() {
530 // Check if this vertex is shared with any unselected rect sector
531 let is_unselected_rect_vertex =
532 map.sectors.iter().any(|sector| {
533 if sector.properties.contains("rect")
534 && !sectors_to_move.contains(§or.id)
535 {
536 sector.linedefs.iter().any(|&line_id| {
537 if let Some(line) =
538 map.find_linedef(line_id)
539 {
540 line.start_vertex == *vertex_id
541 || line.end_vertex == *vertex_id
542 } else {
543 false
544 }
545 })
546 } else {
547 false
548 }
549 });
550
551 if is_unselected_rect_vertex {
552 // Vertex is shared with rect geometry - duplicate it for the sectors being moved
553 if let Some(new_vertex_id) =
554 map.duplicate_vertex(*vertex_id)
555 {
556 // Replace old vertex with new vertex in all sectors being moved
557 for sector_id in sectors_to_move.iter() {
558 map.replace_vertex_in_sector(
559 *sector_id,
560 *vertex_id,
561 new_vertex_id,
562 );
563 }
564 }
565 }
566 }
567 self.vertices_duplicated = true;
568 // Update rectangle_undo_map after duplication so future drags use correct vertex IDs
569 self.rectangle_undo_map = map.clone();
570 }
571
572 // Re-collect vertices from sectors (they may have new IDs after duplication)
573 let mut current_vertices = vec![];
574 for sector_id in sectors_to_move.iter() {
575 if let Some(sector) = map.find_sector(*sector_id) {
576 for line_id in §or.linedefs {
577 if let Some(line) = map.find_linedef(*line_id) {
578 if !current_vertices
579 .contains(&line.start_vertex)
580 {
581 current_vertices.push(line.start_vertex);
582 }
583 if !current_vertices.contains(&line.end_vertex)
584 {
585 current_vertices.push(line.end_vertex);
586 }
587 }
588 }
589 }
590 }
591
592 for vertex_id in current_vertices.iter() {
593 if let Some(original_vertex) =
594 self.rectangle_undo_map.find_vertex(*vertex_id)
595 {
596 let new_x = original_vertex.x + drag_delta.x;
597 let new_y = original_vertex.y + drag_delta.z;
598 let new_z = original_vertex.z + drag_delta.y;
599
600 let subdivisions = 1.0 / map.subdivisions;
601 let snapped_x =
602 (new_x / subdivisions).round() * subdivisions;
603 let snapped_y =
604 (new_y / subdivisions).round() * subdivisions;
605 let snapped_z =
606 (new_z / subdivisions).round() * subdivisions;
607
608 if let Some(vertex) = map.find_vertex_mut(*vertex_id) {
609 vertex.x = snapped_x;
610 vertex.y = snapped_y;
611 vertex.z = snapped_z;
612 }
613 }
614 }
615
616 if drag_delta.x != 0.0
617 || drag_delta.y != 0.0
618 || drag_delta.z != 0.0
619 {
620 self.drag_changed = true;
621 }
622 }
623 }
624 }
625 }
626 } else if let Some(render_view) = ui.get_render_view("PolyView") {
627 if !self.was_clicked {
628 return None;
629 }
630
631 let dim = *render_view.dim();
632 let click_pos = server_ctx.local_to_map_grid(
633 Vec2::new(dim.width as f32, dim.height as f32),
634 self.click_pos,
635 map,
636 map.subdivisions,
637 );
638 let drag_pos = server_ctx.local_to_map_grid(
639 Vec2::new(dim.width as f32, dim.height as f32),
640 Vec2::new(coord.x as f32, coord.y as f32),
641 map,
642 map.subdivisions,
643 );
644
645 let selection = if server_ctx.editor_view_mode == EditorViewMode::D2 {
646 let top_left =
647 Vec2::new(click_pos.x.min(drag_pos.x), click_pos.y.min(drag_pos.y));
648 let bottom_right =
649 Vec2::new(click_pos.x.max(drag_pos.x), click_pos.y.max(drag_pos.y));
650
651 let mut selection =
652 server_ctx.geometry_in_rectangle(top_left, bottom_right, map);
653
654 selection.0 = vec![];
655 selection.1 = vec![];
656 selection
657 } else {
658 let mut selection = (vec![], vec![], vec![]);
659
660 let click_pos = self.click_pos;
661 let drag_pos = Vec2::new(coord.x as f32, coord.y as f32);
662
663 let top_left =
664 Vec2::new(click_pos.x.min(drag_pos.x), click_pos.y.min(drag_pos.y));
665 let bottom_right =
666 Vec2::new(click_pos.x.max(drag_pos.x), click_pos.y.max(drag_pos.y));
667
668 let mut rusterix = RUSTERIX.write().unwrap();
669 rusterix.scene_handler.vm.set_active_vm(2);
670 let linedefs = rusterix.scene_handler.vm.active_vm().pick_geo_ids_in_rect(
671 dim.width as u32,
672 dim.height as u32,
673 top_left,
674 bottom_right,
675 GeoId::Linedef(0),
676 false,
677 false,
678 );
679 for l in linedefs {
680 if let GeoId::Linedef(l) = l {
681 if let Some(linedef) = map.find_linedef(l) {
682 for s in &linedef.sector_ids {
683 if !selection.2.contains(s) {
684 selection.2.push(*s);
685 }
686 }
687 }
688 }
689 }
690 rusterix.scene_handler.vm.set_active_vm(0);
691 selection
692 };
693
694 *map = self.rectangle_undo_map.clone();
695 map.curr_rectangle = Some((click_pos, drag_pos));
696
697 if ui.shift {
698 // Add
699 map.add_to_selection(selection.0, selection.1, selection.2);
700 } else if ui.alt {
701 // Remove
702 map.remove_from_selection(selection.0, selection.1, selection.2);
703 } else {
704 // Replace
705 map.selected_sectors = selection.2;
706 }
707 }
708 crate::editor::RUSTERIX.write().unwrap().set_dirty();
709 }
710 MapUp(_) => {
711 if self.click_selected {
712 if self.drag_changed {
713 undo_atom = Some(ProjectUndoAtom::MapEdit(
714 server_ctx.pc,
715 Box::new(self.rectangle_undo_map.clone()),
716 Box::new(map.clone()),
717 ));
718 ctx.ui.send(TheEvent::Custom(
719 TheId::named("Map Selection Changed"),
720 TheValue::Empty,
721 ));
722 }
723 } else if map.curr_rectangle.is_some() {
724 map.curr_rectangle = None;
725 }
726 self.drag_changed = false;
727 self.click_selected = false;
728 self.was_clicked = false;
729 }
730 MapHover(coord) => {
731 if self.hud.hovered(coord.x, coord.y, map, ui, ctx, server_ctx) {
732 crate::editor::RUSTERIX.write().unwrap().set_dirty();
733 return None;
734 }
735
736 if let Some(render_view) = ui.get_render_view("PolyView") {
737 if server_ctx.editor_view_mode == EditorViewMode::D2 {
738 let dim = *render_view.dim();
739 let h = server_ctx.geometry_at(
740 Vec2::new(dim.width as f32, dim.height as f32),
741 Vec2::new(coord.x as f32, coord.y as f32),
742 map,
743 );
744 server_ctx.hover.2 = h.2;
745
746 let cp = server_ctx.local_to_map_grid(
747 Vec2::new(dim.width as f32, dim.height as f32),
748 Vec2::new(coord.x as f32, coord.y as f32),
749 map,
750 map.subdivisions,
751 );
752 ctx.ui.send(TheEvent::Custom(
753 TheId::named("Cursor Pos Changed"),
754 TheValue::Float2(cp),
755 ));
756 server_ctx.hover_cursor = Some(cp);
757 } else {
758 let hit_pos = server_ctx.hover_cursor_3d.unwrap_or(server_ctx.geo_hit_pos);
759
760 let hovered_sector = match server_ctx.geo_hit {
761 Some(GeoId::Sector(id)) => Some(id),
762 _ => map
763 .find_sector_at(Vec2::new(hit_pos.x, hit_pos.z))
764 .map(|s| s.id),
765 };
766
767 server_ctx.hover = (None, None, hovered_sector);
768
769 if hit_pos != Vec3::zero() {
770 let cp = Vec2::new(hit_pos.x, hit_pos.z);
771 ctx.ui.send(TheEvent::Custom(
772 TheId::named("Cursor Pos Changed"),
773 TheValue::Float2(cp),
774 ));
775 server_ctx.hover_cursor = Some(cp);
776 }
777 }
778
779 if let Some(s) = server_ctx.hover.2 {
780 if let Some(sector) = map.find_sector(s) {
781 let lines = sector
782 .linedefs
783 .iter()
784 .map(|id| id.to_string())
785 .collect::<Vec<_>>()
786 .join(", ");
787 ctx.ui.send(TheEvent::SetStatusText(
788 TheId::empty(),
789 format!("Sector {}: Linedefs ({})", s, lines),
790 ));
791 }
792 } else {
793 ctx.ui
794 .send(TheEvent::SetStatusText(TheId::empty(), "".into()));
795 }
796 }
797 }
798 MapDelete => {
799 if !map.selected_sectors.is_empty() {
800 let prev = map.clone();
801 let sectors = map.selected_sectors.clone();
802
803 #[allow(clippy::useless_vec)]
804 map.delete_elements(&vec![], &vec![], §ors);
805 map.selected_sectors.clear();
806
807 undo_atom = Some(ProjectUndoAtom::MapEdit(
808 server_ctx.pc,
809 Box::new(prev),
810 Box::new(map.clone()),
811 ));
812 ctx.ui.send(TheEvent::Custom(
813 TheId::named("Map Selection Changed"),
814 TheValue::Empty,
815 ));
816 crate::editor::RUSTERIX.write().unwrap().set_dirty();
817 }
818 }
819 MapEscape => {
820 if !map.selected_sectors.is_empty() {
821 map.selected_sectors.clear();
822 ctx.ui.send(TheEvent::Custom(
823 TheId::named("Map Selection Changed"),
824 TheValue::Empty,
825 ));
826 crate::editor::RUSTERIX.write().unwrap().set_dirty();
827 }
828 self.was_clicked = false;
829 }
830 }
831 undo_atom
832 }
833
834 fn draw_hud(
835 &mut self,
836 buffer: &mut TheRGBABuffer,
837 map: &mut Map,
838 ctx: &mut TheContext,
839 server_ctx: &mut ServerContext,
840 assets: &Assets,
841 ) {
842 let id = if !map.selected_sectors.is_empty() {
843 Some(map.selected_sectors[0])
844 } else {
845 None
846 };
847 self.hud.draw(buffer, map, ctx, server_ctx, id, assets);
848 }
849
850 fn handle_event(
851 &mut self,
852 event: &TheEvent,
853 _ui: &mut TheUI,
854 ctx: &mut TheContext,
855 project: &mut Project,
856 server_ctx: &mut ServerContext,
857 ) -> bool {
858 let redraw = false;
859 #[allow(clippy::single_match)]
860 match event {
861 TheEvent::StateChanged(id, state) => {
862 #[allow(clippy::collapsible_if)]
863 if id.name == "Apply Map Properties" && *state == TheWidgetState::Clicked {
864 let mut source: Option<Value> = None;
865
866 if let Some(id) = server_ctx.curr_tile_id {
867 source = Some(Value::Source(PixelSource::TileId(id)));
868 }
869
870 if let Some(source) = source {
871 if let Some(map) = project.get_map_mut(server_ctx) {
872 let _prev = map.clone();
873 for sector_id in &map.selected_sectors.clone() {
874 if let Some(sector) = map.find_sector_mut(*sector_id) {
875 if self.hud.selected_icon_index == 0 {
876 sector.properties.set("source", source.clone());
877 } else if self.hud.selected_icon_index == 1 {
878 sector.properties.set("ceiling_source", source.clone());
879 }
880 }
881 }
882
883 if server_ctx.get_map_context() == MapContext::Region {
884 ctx.ui.send(TheEvent::Custom(
885 TheId::named("Render SceneManager Map"),
886 TheValue::Empty,
887 ));
888 }
889
890 crate::editor::RUSTERIX.write().unwrap().set_dirty();
891 }
892 }
893 } else if id.name == "Remove Map Properties" && *state == TheWidgetState::Clicked {
894 if let Some(map) = project.get_map_mut(server_ctx) {
895 let _prev = map.clone();
896 for sector_id in map.selected_sectors.clone() {
897 if let Some(sector) = map.find_sector_mut(sector_id) {
898 if self.hud.selected_icon_index == 0 {
899 if sector.properties.contains("floor_light") {
900 sector.properties.remove("floor_light");
901 } else {
902 sector
903 .properties
904 .set("source", Value::Source(PixelSource::Off));
905 }
906 } else if self.hud.selected_icon_index == 1 {
907 if sector.properties.contains("ceiling_light") {
908 sector.properties.remove("ceiling_light");
909 } else {
910 sector
911 .properties
912 .set("ceiling_source", Value::Source(PixelSource::Off));
913 }
914 }
915 }
916 }
917
918 if server_ctx.get_map_context() == MapContext::Region {
919 ctx.ui.send(TheEvent::Custom(
920 TheId::named("Render SceneManager Map"),
921 TheValue::Empty,
922 ));
923 }
924
925 crate::editor::RUSTERIX.write().unwrap().set_dirty();
926 }
927 }
928 }
929 _ => {}
930 }
931 redraw
932 }
933}