Skip to main content

rustapi/actions/
create_sector.rs

1use crate::prelude::*;
2use rusterix::Surface;
3
4pub struct CreateSector {
5    id: TheId,
6    nodeui: TheNodeUI,
7}
8
9impl CreateSector {
10    /// Order vertex ids into a closed loop by sorting around the centroid (map XY).
11    fn order_vertices_clockwise(map: &Map, verts: &[u32]) -> Option<Vec<u32>> {
12        if verts.len() < 3 {
13            return None;
14        }
15
16        // Collect unique positions and guard against duplicates
17        let mut pts: Vec<(u32, f32, f32)> = Vec::new();
18        let mut seen = std::collections::HashSet::new();
19        for &vid in verts {
20            if seen.contains(&vid) {
21                continue;
22            }
23            if let Some(v) = map.get_vertex(vid) {
24                // In region maps, XY is the 2D plane; Z is up
25                pts.push((vid, v.x, v.y));
26                seen.insert(vid);
27            }
28        }
29        if pts.len() < 3 {
30            return None;
31        }
32
33        // Centroid in XY
34        let (mut cx, mut cy) = (0.0f32, 0.0f32);
35        for (_, x, y) in &pts {
36            cx += *x;
37            cy += *y;
38        }
39        let n = pts.len() as f32;
40        cx /= n;
41        cy /= n;
42
43        // Sort by angle around centroid; we want CW to keep outer in our editor space
44        pts.sort_by(|a, b| {
45            let aa = (a.2 - cy).atan2(a.1 - cx);
46            let bb = (b.2 - cy).atan2(b.1 - cx);
47            bb.partial_cmp(&aa).unwrap_or(std::cmp::Ordering::Equal)
48        });
49
50        // Drop nearly duplicate neighbors (epsilon)
51        let eps = 1e-5f32;
52        let mut ordered: Vec<u32> = Vec::with_capacity(pts.len());
53        for (i, (vid, x, y)) in pts.iter().enumerate() {
54            let prev = if i == 0 {
55                pts.last().unwrap()
56            } else {
57                &pts[i - 1]
58            };
59            if (prev.1 - *x).abs() + (prev.2 - *y).abs() < eps {
60                continue;
61            }
62            ordered.push(*vid);
63        }
64        if ordered.len() < 3 {
65            return None;
66        }
67        Some(ordered)
68    }
69
70    /// Assemble an ordered closed loop from selected linedefs by walking adjacency.
71    /// Returns ordered vertex ids (without repeating the first at the end).
72    fn loop_from_linedefs(map: &Map, lds: &[u32]) -> Option<Vec<u32>> {
73        use std::collections::{HashMap, HashSet};
74        if lds.len() < 3 {
75            return None;
76        }
77
78        // Build adjacency: vertex -> list of (linedef_id, other_vertex)
79        let mut adj: HashMap<u32, Vec<(u32, u32)>> = HashMap::new();
80        for &ld_id in lds {
81            let ld = map.find_linedef(ld_id)?;
82            let a = ld.start_vertex;
83            let b = ld.end_vertex;
84            adj.entry(a).or_default().push((ld_id, b));
85            adj.entry(b).or_default().push((ld_id, a));
86        }
87        // Single simple cycle requires degree 2 at each vertex
88        if adj.values().any(|v| v.len() != 2) {
89            return None;
90        }
91
92        // Walk the cycle, flipping orientation on the fly
93        let start_ld = lds[0];
94        let ld0 = map.find_linedef(start_ld)?;
95        let start_v = ld0.start_vertex; // arbitrary start
96        let mut ordered: Vec<u32> = Vec::with_capacity(lds.len());
97        let mut used: HashSet<u32> = HashSet::new();
98        let mut curr_v = start_v;
99
100        for _ in 0..lds.len() {
101            // pick a next linedef incident to curr_v that is not used
102            let next = adj
103                .get(&curr_v)?
104                .iter()
105                .find(|(cand_ld, _)| !used.contains(cand_ld))
106                .cloned()?;
107            let (next_ld_id, other_v) = next;
108            used.insert(next_ld_id);
109            ordered.push(curr_v);
110            curr_v = other_v;
111        }
112        // Close check
113        if curr_v != ordered[0] {
114            return None;
115        }
116        Some(ordered)
117    }
118}
119
120impl Action for CreateSector {
121    fn new() -> Self
122    where
123        Self: Sized,
124    {
125        let mut nodeui: TheNodeUI = TheNodeUI::default();
126        let item = TheNodeUIItem::Markdown("desc".into(), fl!("action_create_sector_desc"));
127        nodeui.add_item(item);
128
129        Self {
130            id: TheId::named(&fl!("action_create_sector")),
131            nodeui,
132        }
133    }
134
135    fn id(&self) -> TheId {
136        self.id.clone()
137    }
138
139    fn info(&self) -> String {
140        fl!("action_create_sector_desc")
141    }
142
143    fn role(&self) -> ActionRole {
144        ActionRole::Editor
145    }
146
147    fn accel(&self) -> Option<TheAccelerator> {
148        None
149    }
150
151    fn is_applicable(&self, map: &Map, _ctx: &mut TheContext, _server_ctx: &ServerContext) -> bool {
152        map.selected_vertices.len() >= 3 || map.selected_linedefs.len() >= 3
153    }
154
155    fn apply(
156        &self,
157        map: &mut Map,
158        _ui: &mut TheUI,
159        _ctx: &mut TheContext,
160        server_ctx: &mut ServerContext,
161    ) -> Option<ProjectUndoAtom> {
162        let mut changed = false;
163
164        // Prefer linedefs if available; else use vertices
165        let using_linedefs = !map.selected_linedefs.is_empty();
166        if !using_linedefs && map.selected_vertices.len() < 3 {
167            return None;
168        }
169
170        let prev = map.clone();
171        map.possible_polygon.clear();
172
173        let sector_id: Option<u32>;
174
175        if using_linedefs {
176            // Build an ordered vertex loop from selected linedefs
177            let ordered = match Self::loop_from_linedefs(map, &map.selected_linedefs) {
178                Some(v) => v,
179                None => {
180                    return None;
181                }
182            };
183            // Use manual linedef creation to avoid premature/wrong cycle detection
184            for i in 0..ordered.len() {
185                let a = ordered[i];
186                let b = ordered[(i + 1) % ordered.len()];
187                if a == b {
188                    continue;
189                }
190                let _ = map.create_linedef_manual(a, b);
191            }
192            // Now manually close the polygon
193            sector_id = map.close_polygon_manual();
194        } else {
195            // Vertex-based loop (existing ordering)
196            let ordered = match Self::order_vertices_clockwise(map, &map.selected_vertices) {
197                Some(v) => v,
198                None => {
199                    return None;
200                }
201            };
202            // Use manual linedef creation to avoid premature/wrong cycle detection
203            for i in 0..ordered.len() {
204                let a = ordered[i];
205                let b = ordered[(i + 1) % ordered.len()];
206                if a == b {
207                    continue;
208                }
209                let _ = map.create_linedef_manual(a, b);
210            }
211            // Now manually close the polygon
212            sector_id = map.close_polygon_manual();
213        }
214
215        if let Some(sector_id) = sector_id {
216            map.selected_sectors.clear();
217            map.selected_sectors.push(sector_id);
218            map.possible_polygon.clear();
219
220            let mut surface = Surface::new(sector_id);
221            surface.calculate_geometry(map);
222            map.surfaces.insert(surface.id, surface);
223
224            changed = true;
225        }
226
227        if changed {
228            Some(ProjectUndoAtom::MapEdit(
229                server_ctx.pc,
230                Box::new(prev),
231                Box::new(map.clone()),
232            ))
233        } else {
234            None
235        }
236    }
237
238    fn params(&self) -> TheNodeUI {
239        self.nodeui.clone()
240    }
241
242    fn handle_event(
243        &mut self,
244        event: &TheEvent,
245        _project: &mut Project,
246        _ui: &mut TheUI,
247        _ctx: &mut TheContext,
248        _server_ctx: &mut ServerContext,
249    ) -> bool {
250        self.nodeui.handle_event(event)
251    }
252}