Skip to main content

rustapi/tools/
rect.rs

1use crate::editor::RUSTERIX;
2use crate::hud::{Hud, HudMode};
3use crate::prelude::*;
4use MapEvent::*;
5use ToolEvent::*;
6use rusterix::prelude::*;
7use vek::Vec2;
8
9pub struct RectTool {
10    id: TheId,
11
12    hovered_vertices: Option<[Vec2<f32>; 4]>,
13    mode: i32,
14    hud: Hud,
15
16    processed: FxHashSet<Vec2<i32>>,
17
18    stroke_active: bool,
19    stroke_changed: bool,
20    stroke_prev_map: Option<Map>,
21    stroke_work_map: Option<Map>,
22    last_2d_cell: Option<Vec2<i32>>,
23    line_start_2d_cell: Option<Vec2<i32>>,
24    line_axis_horizontal: Option<bool>,
25}
26
27impl Tool for RectTool {
28    fn new() -> Self
29    where
30        Self: Sized,
31    {
32        Self {
33            id: TheId::named("Rect Tool"),
34
35            hovered_vertices: None,
36            mode: 0,
37            hud: Hud::new(HudMode::Rect),
38
39            processed: FxHashSet::default(),
40
41            stroke_active: false,
42            stroke_changed: false,
43            stroke_prev_map: None,
44            stroke_work_map: None,
45            last_2d_cell: None,
46            line_start_2d_cell: None,
47            line_axis_horizontal: None,
48        }
49    }
50
51    fn id(&self) -> TheId {
52        self.id.clone()
53    }
54    fn info(&self) -> String {
55        fl!("tool_rect")
56    }
57    fn icon_name(&self) -> String {
58        str!("square")
59    }
60    fn accel(&self) -> Option<char> {
61        Some('R')
62    }
63
64    fn help_url(&self) -> Option<String> {
65        Some("docs/creator/tools/rect".to_string())
66    }
67
68    fn tool_event(
69        &mut self,
70        tool_event: ToolEvent,
71        _ui: &mut TheUI,
72        _ctx: &mut TheContext,
73        project: &mut Project,
74        server_ctx: &mut ServerContext,
75    ) -> bool {
76        match tool_event {
77            Activate => {
78                server_ctx.curr_map_tool_type = MapToolType::Rect;
79
80                if let Some(region) = project.get_region_mut(&server_ctx.curr_region) {
81                    region.map.selected_vertices.clear();
82                    region.map.selected_linedefs.clear();
83                    region.map.selected_sectors.clear();
84                }
85
86                // self.activate_map_tool_helper(ui, ctx, project, server_ctx);
87
88                return true;
89            }
90            DeActivate => {
91                server_ctx.curr_map_tool_type = MapToolType::General;
92                server_ctx.hover_cursor = None;
93                self.reset_stroke();
94                if let Some(region) = project.get_region_mut(&server_ctx.curr_region) {
95                    region.map.clear_temp();
96                }
97                return true;
98            }
99            _ => {}
100        };
101
102        false
103    }
104
105    fn map_event(
106        &mut self,
107        map_event: MapEvent,
108        ui: &mut TheUI,
109        ctx: &mut TheContext,
110        map: &mut Map,
111        server_ctx: &mut ServerContext,
112    ) -> Option<ProjectUndoAtom> {
113        let mut undo_atom: Option<ProjectUndoAtom> = None;
114
115        /// Add a tile at the current hover position
116        fn add_tile(
117            ui: &mut TheUI,
118            _ctx: &mut TheContext,
119            map: &mut Map,
120            server_ctx: &mut ServerContext,
121            hovered_vertices: Option<[Vec2<f32>; 4]>,
122            mode: i32,
123        ) -> Option<ProjectUndoAtom> {
124            let mut undo_atom: Option<ProjectUndoAtom> = None;
125            // let size = 1.0 / map.subdivisions;
126
127            if let Some(vertices) = hovered_vertices {
128                let mut add_it = true;
129                let mut layer: u8 = 0;
130
131                if ui.shift {
132                    // Delete the top tile at the given position if shift is pressed
133                    if let Some(ev0) = map.find_vertex_at(vertices[0].x, vertices[0].y) {
134                        if let Some(ev1) = map.find_vertex_at(vertices[1].x, vertices[1].y) {
135                            if let Some(ev2) = map.find_vertex_at(vertices[2].x, vertices[2].y) {
136                                if let Some(ev3) = map.find_vertex_at(vertices[3].x, vertices[3].y)
137                                {
138                                    let sectors =
139                                        map.find_sectors_with_vertex_indices(&[ev0, ev1, ev2, ev3]);
140
141                                    if let Some(sector_id) = sectors.last() {
142                                        let prev = map.clone();
143                                        let mut lines = vec![];
144                                        if let Some(s) = map.find_sector(*sector_id) {
145                                            lines = s.linedefs.clone();
146                                        }
147                                        map.delete_elements(&[], &lines, &[*sector_id]);
148                                        undo_atom = Some(ProjectUndoAtom::MapEdit(
149                                            server_ctx.pc,
150                                            Box::new(prev),
151                                            Box::new(map.clone()),
152                                        ));
153                                    }
154                                }
155                            }
156                        }
157                    }
158                } else if let Some(source) = get_source(ui, server_ctx) {
159                    // Add mode
160                    // Check if tile already exists with same source
161                    if let Some(ev0) = map.find_vertex_at(vertices[0].x, vertices[0].y) {
162                        if let Some(ev1) = map.find_vertex_at(vertices[1].x, vertices[1].y) {
163                            if let Some(ev2) = map.find_vertex_at(vertices[2].x, vertices[2].y) {
164                                if let Some(ev3) = map.find_vertex_at(vertices[3].x, vertices[3].y)
165                                {
166                                    let sectors =
167                                        map.find_sectors_with_vertex_indices(&[ev0, ev1, ev2, ev3]);
168
169                                    let prev = map.clone();
170                                    for sector_id in sectors {
171                                        if let Some(sector) = map.find_sector_mut(sector_id) {
172                                            if let Some(sector_floor_source) =
173                                                sector.properties.get_default_source()
174                                            {
175                                                // Assign id to the higher current layer id (+1).
176                                                if let Some(l) = &sector.layer {
177                                                    if *l > layer {
178                                                        layer = *l;
179                                                    }
180                                                }
181
182                                                if source == *sector_floor_source {
183                                                    // A tile with the same floor_source exists, do not add.
184                                                    add_it = false;
185                                                } else if mode == 0 {
186                                                    // In overlay mode we just overwrite the source
187                                                    sector.properties.set(
188                                                        "source",
189                                                        Value::Source(source.clone()),
190                                                    );
191
192                                                    undo_atom = Some(ProjectUndoAtom::MapEdit(
193                                                        server_ctx.pc,
194                                                        Box::new(prev),
195                                                        Box::new(map.clone()),
196                                                    ));
197
198                                                    add_it = false;
199                                                    break;
200                                                }
201                                            }
202                                        }
203                                    }
204                                }
205                            }
206                        }
207                    }
208
209                    if add_it {
210                        let v0 = map.add_vertex_at(vertices[0].x, vertices[0].y);
211                        let v1 = map.add_vertex_at(vertices[1].x, vertices[1].y);
212                        let v2 = map.add_vertex_at(vertices[2].x, vertices[2].y);
213                        let v3 = map.add_vertex_at(vertices[3].x, vertices[3].y);
214
215                        map.possible_polygon = vec![];
216                        let _ = map.create_linedef_manual(v0, v1);
217                        let _ = map.create_linedef_manual(v1, v2);
218                        let _ = map.create_linedef_manual(v2, v3);
219                        let _ = map.create_linedef_manual(v3, v0);
220                        let sid = map.close_polygon_manual();
221
222                        if let Some(sector_id) = sid {
223                            // Add the info for correct box rendering
224                            // if let Some(l) = map.find_linedef_mut(l0.0) {
225                            //     l.properties.set("row1_source", source.clone());
226                            //     l.properties.set("wall_height", Value::Float(size));
227                            // }
228                            // if let Some(l) = map.find_linedef_mut(l1.0) {
229                            //     l.properties.set("row1_source", source.clone());
230                            //     l.properties.set("wall_height", Value::Float(size));
231                            // }
232                            // if let Some(l) = map.find_linedef_mut(l2.0) {
233                            //     l.properties.set("row1_source", source.clone());
234                            //     l.properties.set("wall_height", Value::Float(size));
235                            // }
236                            // if let Some(l) = map.find_linedef_mut(id.0) {
237                            //     l.properties.set("row1_source", source.clone());
238                            //     l.properties.set("wall_height", Value::Float(size));
239                            // }
240
241                            let prev = map.clone();
242                            if let Some(sector) = map.find_sector_mut(sector_id) {
243                                sector.properties.set("rect", Value::Bool(true));
244
245                                sector.properties.set("source", Value::Source(source));
246                                sector.layer = Some(layer + 1);
247                            }
248
249                            undo_atom = Some(ProjectUndoAtom::MapEdit(
250                                server_ctx.pc,
251                                Box::new(prev),
252                                Box::new(map.clone()),
253                            ));
254
255                            map.selected_vertices.clear();
256                            map.selected_linedefs.clear();
257                            map.selected_sectors = vec![sector_id];
258                        }
259                    }
260                }
261            }
262
263            undo_atom
264        }
265
266        fn apply_hover(
267            coord: Vec2<i32>,
268            ui: &mut TheUI,
269            _ctx: &mut TheContext,
270            map: &mut Map,
271            server_ctx: &mut ServerContext,
272        ) -> Option<[Vec2<f32>; 4]> {
273            let mut hovered_vertices: Option<[Vec2<f32>; 4]> = None;
274
275            if let Some(render_view) = ui.get_render_view("PolyView") {
276                let dim = *render_view.dim();
277                server_ctx.hover = (None, None, None);
278                let cp = server_ctx.local_to_map_cell(
279                    Vec2::new(dim.width as f32, dim.height as f32),
280                    Vec2::new(coord.x as f32, coord.y as f32),
281                    map,
282                    1.0,
283                );
284                let step = 1.0;
285                map.curr_rectangle = Some((cp, cp + step));
286                hovered_vertices = Some([
287                    cp,
288                    cp + Vec2::new(0.0, step),
289                    cp + Vec2::new(step, step),
290                    cp + Vec2::new(step, 0.0),
291                ]);
292                server_ctx.hover_cursor = Some(cp);
293                if map.properties.get_bool_default("terrain_enabled", false) {
294                    server_ctx.rect_terrain_id = Some((cp.x.floor() as i32, cp.y.floor() as i32));
295                } else {
296                    server_ctx.rect_terrain_id = None;
297                }
298            }
299
300            hovered_vertices
301        }
302
303        match map_event {
304            MapKey(_c) => {}
305            MapClicked(coord) => {
306                if self.hud.clicked(coord.x, coord.y, map, ui, ctx, server_ctx) {
307                    crate::editor::RUSTERIX.write().unwrap().set_dirty();
308                    return None;
309                }
310
311                self.processed.clear();
312                if server_ctx.editor_view_mode == EditorViewMode::D2 {
313                    let use_terrain_paint =
314                        map.properties.get_bool_default("terrain_enabled", false);
315                    if let Some(cp) = server_ctx.hover_cursor {
316                        self.begin_stroke_if_needed(map);
317                        let k = if use_terrain_paint {
318                            Vec2::new(cp.x.floor() as i32, cp.y.floor() as i32)
319                        } else {
320                            Vec2::new(cp.x.floor() as i32, cp.y.floor() as i32)
321                        };
322                        if ui.ctrl && !use_terrain_paint {
323                            self.line_start_2d_cell = Some(k);
324                            self.line_axis_horizontal = None;
325                        }
326                        if let Some(work_map) = self.stroke_work_map.as_mut() {
327                            let changed = if use_terrain_paint {
328                                server_ctx.rect_terrain_id = Some((k.x, k.y));
329                                Self::apply_3d_paint_at_current_target(work_map, ui, server_ctx)
330                                    .is_some()
331                            } else {
332                                let step = 1.0;
333                                let x = k.x as f32 * step;
334                                let y = k.y as f32 * step;
335                                let verts = Some([
336                                    Vec2::new(x, y),
337                                    Vec2::new(x, y + step),
338                                    Vec2::new(x + step, y + step),
339                                    Vec2::new(x + step, y),
340                                ]);
341                                add_tile(ui, ctx, work_map, server_ctx, verts, self.mode).is_some()
342                            };
343                            if changed {
344                                self.stroke_changed = true;
345                            }
346                            self.processed.insert(k);
347                            self.last_2d_cell = Some(k);
348                        }
349                    }
350                } else {
351                    self.compute_3d_tile(coord, map, ui, server_ctx);
352                    self.begin_stroke_if_needed(map);
353                    if let Some(work_map) = self.stroke_work_map.as_mut()
354                        && let Some(key) =
355                            Self::apply_3d_paint_at_current_target(work_map, ui, server_ctx)
356                        && !self.processed.contains(&key)
357                    {
358                        self.stroke_changed = true;
359                        self.processed.insert(key);
360                    }
361                }
362            }
363            MapDragged(coord) => {
364                if self.hud.dragged(coord.x, coord.y, map, ui, ctx, server_ctx) {
365                    crate::editor::RUSTERIX.write().unwrap().set_dirty();
366                    return None;
367                }
368                if server_ctx.editor_view_mode == EditorViewMode::D2 {
369                    let use_terrain_paint =
370                        map.properties.get_bool_default("terrain_enabled", false);
371                    self.hovered_vertices = apply_hover(coord, ui, ctx, map, server_ctx);
372                    if let Some(cp) = server_ctx.hover_cursor {
373                        self.begin_stroke_if_needed(map);
374                        let k = if use_terrain_paint {
375                            Vec2::new(cp.x.floor() as i32, cp.y.floor() as i32)
376                        } else {
377                            Vec2::new(cp.x.floor() as i32, cp.y.floor() as i32)
378                        };
379                        if let Some(work_map) = self.stroke_work_map.as_mut() {
380                            let line_anchor = self.line_start_2d_cell.unwrap_or(k);
381                            let mut to = k;
382                            if ui.ctrl && !use_terrain_paint {
383                                if self.line_axis_horizontal.is_none() {
384                                    let dx = (to.x - line_anchor.x).abs();
385                                    let dy = (to.y - line_anchor.y).abs();
386                                    if dx > 0 || dy > 0 {
387                                        self.line_axis_horizontal = Some(dx >= dy);
388                                    }
389                                }
390                                if let Some(horizontal) = self.line_axis_horizontal {
391                                    if horizontal {
392                                        to.y = line_anchor.y;
393                                    } else {
394                                        to.x = line_anchor.x;
395                                    }
396                                }
397                            }
398
399                            let from = self.last_2d_cell.unwrap_or(to);
400                            let step = 1.0;
401                            let between = Self::cells_between(from, to);
402                            for cell in between.iter().copied() {
403                                if self.processed.contains(&cell) {
404                                    continue;
405                                }
406                                let changed = if use_terrain_paint {
407                                    server_ctx.rect_terrain_id = Some((cell.x, cell.y));
408                                    Self::apply_3d_paint_at_current_target(work_map, ui, server_ctx)
409                                        .is_some()
410                                } else {
411                                    let x = cell.x as f32 * step;
412                                    let y = cell.y as f32 * step;
413                                    let verts = Some([
414                                        Vec2::new(x, y),
415                                        Vec2::new(x, y + step),
416                                        Vec2::new(x + step, y + step),
417                                        Vec2::new(x + step, y),
418                                    ]);
419                                    add_tile(ui, ctx, work_map, server_ctx, verts, self.mode)
420                                        .is_some()
421                                };
422                                if changed {
423                                    self.stroke_changed = true;
424                                }
425                                self.processed.insert(cell);
426                            }
427                            self.last_2d_cell = Some(to);
428                        }
429                    }
430                } else {
431                    self.compute_3d_tile(coord, map, ui, server_ctx);
432                    self.begin_stroke_if_needed(map);
433                    if let Some(work_map) = self.stroke_work_map.as_mut()
434                        && let Some(key) =
435                            Self::apply_3d_paint_at_current_target(work_map, ui, server_ctx)
436                        && !self.processed.contains(&key)
437                    {
438                        self.stroke_changed = true;
439                        self.processed.insert(key);
440                    }
441                }
442            }
443            MapUp(_) => {
444                if self.stroke_active {
445                    if self.stroke_changed
446                        && let (Some(prev), Some(new_map)) =
447                            (self.stroke_prev_map.take(), self.stroke_work_map.take())
448                    {
449                        *map = new_map;
450                        undo_atom = Some(ProjectUndoAtom::MapEdit(
451                            server_ctx.pc,
452                            Box::new(prev),
453                            Box::new(map.clone()),
454                        ));
455                    }
456                    self.reset_stroke();
457                }
458            }
459            MapHover(coord) => {
460                if server_ctx.editor_view_mode == EditorViewMode::D2 {
461                    self.hovered_vertices = apply_hover(coord, ui, ctx, map, server_ctx);
462                } else {
463                    self.compute_3d_tile(coord, map, ui, server_ctx);
464                }
465            }
466            MapDelete => {}
467            MapEscape => {
468                self.reset_stroke();
469                map.clear_temp();
470                crate::editor::RUSTERIX.write().unwrap().set_dirty();
471            }
472        }
473        undo_atom
474    }
475
476    fn draw_hud(
477        &mut self,
478        buffer: &mut TheRGBABuffer,
479        map: &mut Map,
480        ctx: &mut TheContext,
481        server_ctx: &mut ServerContext,
482        assets: &Assets,
483    ) {
484        let id = if !map.selected_linedefs.is_empty() {
485            Some(map.selected_linedefs[0])
486        } else {
487            None
488        };
489        self.hud.draw(buffer, map, ctx, server_ctx, id, assets);
490    }
491
492    fn handle_event(
493        &mut self,
494        event: &TheEvent,
495        _ui: &mut TheUI,
496        _ctx: &mut TheContext,
497        project: &mut Project,
498        server_ctx: &mut ServerContext,
499    ) -> bool {
500        let redraw = false;
501        #[allow(clippy::single_match)]
502        match event {
503            TheEvent::StateChanged(id, state) => {
504                #[allow(clippy::collapsible_if)]
505                if id.name == "Apply Map Properties" && *state == TheWidgetState::Clicked {
506                    let mut source: Option<Value> = None;
507
508                    if let Some(id) = server_ctx.curr_tile_id {
509                        source = Some(Value::Source(PixelSource::TileId(id)));
510                    }
511
512                    if let Some(source) = source {
513                        if let Some(map) = project.get_map_mut(server_ctx) {
514                            let _prev = map.clone();
515
516                            for linedef_id in map.selected_linedefs.clone() {
517                                if let Some(linedef) = map.find_linedef_mut(linedef_id) {
518                                    if self.hud.selected_icon_index == 0 {
519                                        linedef.properties.set("row1_source", source.clone());
520                                    } else if self.hud.selected_icon_index == 1 {
521                                        linedef.properties.set("row2_source", source.clone());
522                                    } else if self.hud.selected_icon_index == 2 {
523                                        linedef.properties.set("row3_source", source.clone());
524                                    } else if self.hud.selected_icon_index == 3 {
525                                        linedef.properties.set("row4_source", source.clone());
526                                    }
527                                    crate::editor::RUSTERIX.write().unwrap().set_dirty();
528                                }
529                            }
530
531                            crate::editor::RUSTERIX.write().unwrap().set_dirty();
532                        }
533                    }
534                } else if id.name == "Remove Map Properties" && *state == TheWidgetState::Clicked {
535                    if let Some(map) = project.get_map_mut(server_ctx) {
536                        let _prev = map.clone();
537
538                        for linedef_id in map.selected_linedefs.clone() {
539                            if let Some(linedef) = map.find_linedef_mut(linedef_id) {
540                                if self.hud.selected_icon_index == 0 {
541                                    linedef
542                                        .properties
543                                        .set("row1_source", Value::Source(PixelSource::Off));
544                                } else if self.hud.selected_icon_index == 1 {
545                                    linedef
546                                        .properties
547                                        .set("row2_source", Value::Source(PixelSource::Off));
548                                } else if self.hud.selected_icon_index == 2 {
549                                    linedef
550                                        .properties
551                                        .set("row3_source", Value::Source(PixelSource::Off));
552                                } else if self.hud.selected_icon_index == 3 {
553                                    linedef
554                                        .properties
555                                        .set("row4_source", Value::Source(PixelSource::Off));
556                                }
557                                crate::editor::RUSTERIX.write().unwrap().set_dirty();
558                            }
559                        }
560
561                        crate::editor::RUSTERIX.write().unwrap().set_dirty();
562                    }
563                }
564            }
565            _ => {}
566        }
567        redraw
568    }
569}
570
571impl RectTool {
572    fn begin_stroke_if_needed(&mut self, map: &Map) {
573        if !self.stroke_active {
574            self.stroke_active = true;
575            self.stroke_changed = false;
576            self.stroke_prev_map = Some(map.clone());
577            self.stroke_work_map = Some(map.clone());
578            self.processed.clear();
579        }
580    }
581
582    fn reset_stroke(&mut self) {
583        self.stroke_active = false;
584        self.stroke_changed = false;
585        self.stroke_prev_map = None;
586        self.stroke_work_map = None;
587        self.last_2d_cell = None;
588        self.line_start_2d_cell = None;
589        self.line_axis_horizontal = None;
590        self.processed.clear();
591    }
592
593    fn cells_between(a: Vec2<i32>, b: Vec2<i32>) -> Vec<Vec2<i32>> {
594        let mut out = Vec::new();
595        let mut x0 = a.x;
596        let mut y0 = a.y;
597        let x1 = b.x;
598        let y1 = b.y;
599
600        let dx = (x1 - x0).abs();
601        let sx = if x0 < x1 { 1 } else { -1 };
602        let dy = -(y1 - y0).abs();
603        let sy = if y0 < y1 { 1 } else { -1 };
604        let mut err = dx + dy;
605
606        loop {
607            out.push(Vec2::new(x0, y0));
608            if x0 == x1 && y0 == y1 {
609                break;
610            }
611            let e2 = 2 * err;
612            if e2 >= dy {
613                err += dy;
614                x0 += sx;
615            }
616            if e2 <= dx {
617                err += dx;
618                y0 += sy;
619            }
620        }
621
622        out
623    }
624
625    fn apply_3d_paint_at_current_target(
626        map: &mut Map,
627        ui: &TheUI,
628        server_ctx: &ServerContext,
629    ) -> Option<Vec2<i32>> {
630        let curr_tile_id = server_ctx.curr_tile_id;
631
632        if let Some((x, z)) = server_ctx.rect_terrain_id {
633            let mut tiles = match map.properties.get("tiles") {
634                Some(Value::TileOverrides(existing)) => existing.clone(),
635                _ => FxHashMap::default(),
636            };
637
638            let mut blend_tiles = match map.properties.get("blend_tiles") {
639                Some(Value::BlendOverrides(existing)) => existing.clone(),
640                _ => FxHashMap::default(),
641            };
642
643            if ui.shift {
644                tiles.remove(&(x, z));
645                blend_tiles.remove(&(x, z));
646            } else if let Some(tile_id) = curr_tile_id {
647                if server_ctx.rect_blend_preset == VertexBlendPreset::Solid {
648                    tiles.insert((x, z), PixelSource::TileId(tile_id));
649                    blend_tiles.remove(&(x, z));
650                } else {
651                    blend_tiles.insert(
652                        (x, z),
653                        (server_ctx.rect_blend_preset, PixelSource::TileId(tile_id)),
654                    );
655                    tiles.remove(&(x, z));
656                }
657            } else {
658                return None;
659            }
660
661            if tiles.is_empty() {
662                map.properties.remove("tiles");
663            } else {
664                map.properties.set("tiles", Value::TileOverrides(tiles));
665            }
666            if blend_tiles.is_empty() {
667                map.properties.remove("blend_tiles");
668            } else {
669                map.properties
670                    .set("blend_tiles", Value::BlendOverrides(blend_tiles));
671            }
672
673            return Some(Vec2::new(x, z));
674        }
675
676        if let Some(sector_id) = server_ctx.rect_sector_id_3d
677            && let Some(sector) = map.find_sector_mut(sector_id)
678        {
679            let key = Vec2::new(server_ctx.rect_tile_id_3d.0, server_ctx.rect_tile_id_3d.1);
680
681            let mut tiles = match sector.properties.get("tiles") {
682                Some(Value::TileOverrides(existing)) => existing.clone(),
683                _ => FxHashMap::default(),
684            };
685            let mut blend_tiles = match sector.properties.get("blend_tiles") {
686                Some(Value::BlendOverrides(existing)) => existing.clone(),
687                _ => FxHashMap::default(),
688            };
689
690            if ui.shift {
691                tiles.remove(&server_ctx.rect_tile_id_3d);
692                blend_tiles.remove(&server_ctx.rect_tile_id_3d);
693            } else if let Some(tile_id) = curr_tile_id {
694                if server_ctx.rect_blend_preset == VertexBlendPreset::Solid {
695                    tiles.insert(server_ctx.rect_tile_id_3d, PixelSource::TileId(tile_id));
696                    blend_tiles.remove(&server_ctx.rect_tile_id_3d);
697                } else {
698                    blend_tiles.insert(
699                        server_ctx.rect_tile_id_3d,
700                        (server_ctx.rect_blend_preset, PixelSource::TileId(tile_id)),
701                    );
702                    tiles.remove(&server_ctx.rect_tile_id_3d);
703                }
704            } else {
705                return None;
706            }
707
708            if tiles.is_empty() {
709                sector.properties.remove("tiles");
710            } else {
711                sector.properties.set("tiles", Value::TileOverrides(tiles));
712            }
713            if blend_tiles.is_empty() {
714                sector.properties.remove("blend_tiles");
715            } else {
716                sector
717                    .properties
718                    .set("blend_tiles", Value::BlendOverrides(blend_tiles));
719            }
720
721            return Some(key);
722        }
723
724        None
725    }
726
727    fn compute_3d_tile(
728        &mut self,
729        coord: Vec2<i32>,
730        map: &Map,
731        ui: &mut TheUI,
732        server_ctx: &mut ServerContext,
733    ) {
734        if let Some(render_view) = ui.get_render_view("PolyView") {
735            let dim = *render_view.dim();
736
737            let screen_uv = [
738                coord.x as f32 / dim.width as f32,
739                coord.y as f32 / dim.height as f32,
740            ];
741
742            let rusterix = RUSTERIX.read().unwrap();
743
744            let rc = rusterix.scene_handler.vm.pick_geo_id_at_uv(
745                dim.width as u32,
746                dim.height as u32,
747                screen_uv,
748                false,
749                false,
750            );
751
752            let mut found = false;
753            if let Some((scenevm::GeoId::Sector(id), world_hit, _)) = rc {
754                let mut best: Option<((i32, i32), f32)> = None;
755
756                for surface in map.surfaces.values() {
757                    if surface.sector_id != id {
758                        continue;
759                    }
760
761                    let n = surface.plane.normal;
762                    let n_len = n.magnitude();
763                    if n_len <= 1e-6 {
764                        continue;
765                    }
766
767                    // Choose the surface plane closest to the picked world hit.
768                    // This avoids random sector-surface iteration order affecting tile coordinates.
769                    let signed_dist = (world_hit - surface.plane.origin).dot(n / n_len);
770                    let dist = signed_dist.abs();
771                    let tile = surface.world_to_tile_local(world_hit, map);
772
773                    if best
774                        .as_ref()
775                        .map(|(_, best_dist)| dist < *best_dist)
776                        .unwrap_or(true)
777                    {
778                        best = Some((tile, dist));
779                    }
780                }
781
782                if let Some((tile, _)) = best {
783                    server_ctx.rect_tile_id_3d = tile;
784                    server_ctx.rect_sector_id_3d = Some(id);
785                    found = true;
786                }
787            }
788
789            if let Some((scenevm::GeoId::Terrain(x, z), _, _)) = rc {
790                server_ctx.rect_terrain_id = Some((x, z));
791            } else {
792                server_ctx.rect_terrain_id = None;
793            }
794
795            if !found {
796                server_ctx.rect_sector_id_3d = None;
797            }
798        }
799    }
800}