Skip to main content

rustapi/actions/
duplicate.rs

1use crate::prelude::*;
2use rusterix::{Linedef, Sector, Surface};
3
4pub const DUPLICATE_ACTION_ID: &str = "1468f85f-ef66-49f9-8c3f-54fbde6e3d9c";
5
6pub struct Duplicate {
7    id: TheId,
8    nodeui: TheNodeUI,
9}
10
11impl Action for Duplicate {
12    fn new() -> Self
13    where
14        Self: Sized,
15    {
16        let mut nodeui: TheNodeUI = TheNodeUI::default();
17
18        nodeui.add_item(TheNodeUIItem::FloatEditSlider(
19            "actionDuplicateX".into(),
20            "".into(),
21            "".into(),
22            0.0,
23            -1000.0..=1000.0,
24            false,
25        ));
26        nodeui.add_item(TheNodeUIItem::FloatEditSlider(
27            "actionDuplicateY".into(),
28            "".into(),
29            "".into(),
30            1.0,
31            -1000.0..=1000.0,
32            false,
33        ));
34        nodeui.add_item(TheNodeUIItem::FloatEditSlider(
35            "actionDuplicateZ".into(),
36            "".into(),
37            "".into(),
38            0.0,
39            -1000.0..=1000.0,
40            false,
41        ));
42        nodeui.add_item(TheNodeUIItem::OpenTree("sector".into()));
43        nodeui.add_item(TheNodeUIItem::Checkbox(
44            "actionSectorConnect".into(),
45            "".into(),
46            "".into(),
47            false,
48        ));
49        nodeui.add_item(TheNodeUIItem::CloseTree);
50
51        Self {
52            id: TheId::named_with_id(
53                &fl!("action_duplicate"),
54                Uuid::parse_str(DUPLICATE_ACTION_ID).unwrap(),
55            ),
56            nodeui,
57        }
58    }
59
60    fn id(&self) -> TheId {
61        self.id.clone()
62    }
63
64    fn info(&self) -> String {
65        fl!("action_duplicate_desc")
66    }
67
68    fn role(&self) -> ActionRole {
69        ActionRole::Editor
70    }
71
72    fn is_applicable(&self, map: &Map, _ctx: &mut TheContext, _server_ctx: &ServerContext) -> bool {
73        !map.selected_vertices.is_empty()
74            || !map.selected_linedefs.is_empty()
75            || !map.selected_sectors.is_empty()
76    }
77
78    fn apply(
79        &self,
80        map: &mut Map,
81        _ui: &mut TheUI,
82        _ctx: &mut TheContext,
83        server_ctx: &mut ServerContext,
84    ) -> Option<ProjectUndoAtom> {
85        if map.selected_vertices.is_empty()
86            && map.selected_linedefs.is_empty()
87            && map.selected_sectors.is_empty()
88        {
89            return None;
90        }
91
92        let prev = map.clone();
93
94        // Match editor XYZ convention: Y maps to vertex.z and Z maps to vertex.y (vertical).
95        let offset_x = self.nodeui.get_f32_value("actionDuplicateX").unwrap_or(0.0);
96        let offset_y = self.nodeui.get_f32_value("actionDuplicateY").unwrap_or(0.0);
97        let offset_z = self.nodeui.get_f32_value("actionDuplicateZ").unwrap_or(1.0);
98        let connect_sectors = self
99            .nodeui
100            .get_bool_value("actionSectorConnect")
101            .unwrap_or(false);
102
103        let mut selected_sector_ids = map.selected_sectors.clone();
104        selected_sector_ids.sort_unstable();
105        let mut selected_linedef_ids = map.selected_linedefs.clone();
106        selected_linedef_ids.sort_unstable();
107        let mut selected_vertex_ids = map.selected_vertices.clone();
108        selected_vertex_ids.sort_unstable();
109        selected_vertex_ids.dedup();
110
111        let mut old_linedef_ids: FxHashSet<u32> = FxHashSet::default();
112        for linedef_id in &selected_linedef_ids {
113            old_linedef_ids.insert(*linedef_id);
114        }
115        for sector_id in &selected_sector_ids {
116            if let Some(sector) = map.find_sector(*sector_id) {
117                for linedef_id in &sector.linedefs {
118                    old_linedef_ids.insert(*linedef_id);
119                }
120            }
121        }
122
123        let mut old_vertex_ids: FxHashSet<u32> = FxHashSet::default();
124        for vertex_id in &selected_vertex_ids {
125            old_vertex_ids.insert(*vertex_id);
126        }
127        for linedef_id in &old_linedef_ids {
128            if let Some(linedef) = map.find_linedef(*linedef_id) {
129                old_vertex_ids.insert(linedef.start_vertex);
130                old_vertex_ids.insert(linedef.end_vertex);
131            }
132        }
133
134        let mut sorted_vertex_ids: Vec<u32> = old_vertex_ids.into_iter().collect();
135        sorted_vertex_ids.sort_unstable();
136
137        let mut sorted_linedef_ids: Vec<u32> = old_linedef_ids.into_iter().collect();
138        sorted_linedef_ids.sort_unstable();
139
140        let mut next_vertex_id = map.vertices.iter().map(|v| v.id).max().unwrap_or(0);
141        let mut next_linedef_id = map.linedefs.iter().map(|l| l.id).max().unwrap_or(0);
142        let mut next_sector_id = map.sectors.iter().map(|s| s.id).max().unwrap_or(0);
143
144        let mut vertex_map: FxHashMap<u32, u32> = FxHashMap::default();
145        let mut linedef_map: FxHashMap<u32, u32> = FxHashMap::default();
146
147        let mut new_vertices = Vec::new();
148        let mut new_linedefs = Vec::new();
149        let mut new_sectors = Vec::new();
150        let mut sector_map: FxHashMap<u32, u32> = FxHashMap::default();
151
152        for old_vid in sorted_vertex_ids {
153            if let Some(old_vertex) = map.find_vertex(old_vid).cloned() {
154                next_vertex_id = next_vertex_id.saturating_add(1);
155                let new_id = next_vertex_id;
156                let mut new_vertex = old_vertex;
157                new_vertex.id = new_id;
158                new_vertex.x += offset_x;
159                new_vertex.y += offset_z;
160                new_vertex.z += offset_y;
161                vertex_map.insert(old_vid, new_id);
162                new_vertices.push(new_vertex);
163            }
164        }
165
166        for old_lid in sorted_linedef_ids {
167            if let Some(old_linedef) = map.find_linedef(old_lid).cloned()
168                && let (Some(&new_start), Some(&new_end)) = (
169                    vertex_map.get(&old_linedef.start_vertex),
170                    vertex_map.get(&old_linedef.end_vertex),
171                )
172            {
173                next_linedef_id = next_linedef_id.saturating_add(1);
174                let new_id = next_linedef_id;
175                let mut new_linedef = old_linedef;
176                new_linedef.id = new_id;
177                new_linedef.start_vertex = new_start;
178                new_linedef.end_vertex = new_end;
179                new_linedef.sector_ids.clear();
180                linedef_map.insert(old_lid, new_id);
181                new_linedefs.push(new_linedef);
182            }
183        }
184
185        for old_sid in &selected_sector_ids {
186            if let Some(old_sector) = map.find_sector(*old_sid).cloned() {
187                next_sector_id = next_sector_id.saturating_add(1);
188                let new_id = next_sector_id;
189                let mut new_sector = old_sector;
190                new_sector.id = new_id;
191                new_sector.linedefs = new_sector
192                    .linedefs
193                    .iter()
194                    .filter_map(|id| linedef_map.get(id).copied())
195                    .collect();
196                new_sectors.push(new_sector);
197                sector_map.insert(*old_sid, new_id);
198            }
199        }
200
201        if connect_sectors {
202            let selected_sector_set: FxHashSet<u32> = selected_sector_ids.iter().copied().collect();
203            let mut connector_linedefs = Vec::new();
204            let mut connector_sectors = Vec::new();
205
206            for old_sid in &selected_sector_ids {
207                let Some(old_sector) = map.find_sector(*old_sid).cloned() else {
208                    continue;
209                };
210                if !sector_map.contains_key(old_sid) {
211                    continue;
212                }
213
214                for old_linedef_id in old_sector.linedefs {
215                    let Some(old_linedef) = map.find_linedef(old_linedef_id) else {
216                        continue;
217                    };
218                    // Skip interior edges when duplicating multiple touching sectors.
219                    let is_internal = old_linedef.sector_ids.len() > 1
220                        && old_linedef
221                            .sector_ids
222                            .iter()
223                            .all(|sid| selected_sector_set.contains(sid));
224                    if is_internal {
225                        continue;
226                    }
227
228                    let Some(&new_start) = vertex_map.get(&old_linedef.start_vertex) else {
229                        continue;
230                    };
231                    let Some(&new_end) = vertex_map.get(&old_linedef.end_vertex) else {
232                        continue;
233                    };
234
235                    next_linedef_id = next_linedef_id.saturating_add(1);
236                    let bridge_side_a_id = next_linedef_id;
237                    let mut bridge_side_a =
238                        Linedef::new(bridge_side_a_id, old_linedef.end_vertex, new_end);
239
240                    next_linedef_id = next_linedef_id.saturating_add(1);
241                    let bridge_side_b_id = next_linedef_id;
242                    let mut bridge_side_b =
243                        Linedef::new(bridge_side_b_id, new_start, old_linedef.start_vertex);
244
245                    // Use a dedicated reversed copy of the duplicated top edge so the connector
246                    // sector keeps a proper vertex loop order (A -> B -> B' -> A').
247                    next_linedef_id = next_linedef_id.saturating_add(1);
248                    let bridge_top_id = next_linedef_id;
249                    let mut bridge_top = Linedef::new(bridge_top_id, new_end, new_start);
250
251                    next_sector_id = next_sector_id.saturating_add(1);
252                    let connector_sector_id = next_sector_id;
253                    bridge_side_a.sector_ids.push(connector_sector_id);
254                    bridge_side_b.sector_ids.push(connector_sector_id);
255                    bridge_top.sector_ids.push(connector_sector_id);
256
257                    let connector_sector = Sector::new(
258                        connector_sector_id,
259                        vec![
260                            old_linedef_id,
261                            bridge_side_a_id,
262                            bridge_top_id,
263                            bridge_side_b_id,
264                        ],
265                    );
266
267                    connector_linedefs.push(bridge_side_a);
268                    connector_linedefs.push(bridge_side_b);
269                    connector_linedefs.push(bridge_top);
270                    connector_sectors.push(connector_sector);
271                }
272            }
273
274            new_linedefs.extend(connector_linedefs);
275            new_sectors.extend(connector_sectors);
276        }
277
278        for new_sector in &new_sectors {
279            for new_linedef_id in &new_sector.linedefs {
280                if let Some(new_linedef) = new_linedefs.iter_mut().find(|l| l.id == *new_linedef_id)
281                    && !new_linedef.sector_ids.contains(&new_sector.id)
282                {
283                    new_linedef.sector_ids.push(new_sector.id);
284                } else if let Some(existing_linedef) = map.find_linedef_mut(*new_linedef_id)
285                    && !existing_linedef.sector_ids.contains(&new_sector.id)
286                {
287                    existing_linedef.sector_ids.push(new_sector.id);
288                }
289            }
290        }
291
292        if new_vertices.is_empty() && new_linedefs.is_empty() && new_sectors.is_empty() {
293            return None;
294        }
295
296        map.vertices.extend(new_vertices.clone());
297        map.linedefs.extend(new_linedefs.clone());
298        map.sectors.extend(new_sectors.clone());
299
300        // Ensure duplicated/connector sectors have matching surfaces so they render in 3D.
301        for sector in &new_sectors {
302            if map.get_surface_for_sector_id(sector.id).is_none() {
303                let mut surface = if let Some((&old_sid, _)) = sector_map
304                    .iter()
305                    .find(|(_, new_sid)| **new_sid == sector.id)
306                {
307                    if let Some(src_surface) = map.get_surface_for_sector_id(old_sid) {
308                        let mut cloned = src_surface.clone();
309                        cloned.id = Uuid::new_v4();
310                        cloned.sector_id = sector.id;
311                        cloned
312                    } else {
313                        Surface::new(sector.id)
314                    }
315                } else {
316                    Surface::new(sector.id)
317                };
318                surface.calculate_geometry(map);
319                map.surfaces.insert(surface.id, surface);
320            }
321        }
322
323        map.selected_vertices = selected_vertex_ids
324            .iter()
325            .filter_map(|id| vertex_map.get(id).copied())
326            .collect();
327
328        map.selected_linedefs = selected_linedef_ids
329            .iter()
330            .filter_map(|id| linedef_map.get(id).copied())
331            .collect();
332
333        // For selected sectors we duplicated all their linedefs, so one-to-one order mapping is valid.
334        map.selected_sectors = new_sectors.iter().map(|s| s.id).collect();
335
336        Some(ProjectUndoAtom::MapEdit(
337            server_ctx.pc,
338            Box::new(prev),
339            Box::new(map.clone()),
340        ))
341    }
342
343    fn params(&self) -> TheNodeUI {
344        self.nodeui.clone()
345    }
346
347    fn handle_event(
348        &mut self,
349        event: &TheEvent,
350        _project: &mut Project,
351        _ui: &mut TheUI,
352        _ctx: &mut TheContext,
353        _server_ctx: &mut ServerContext,
354    ) -> bool {
355        self.nodeui.handle_event(event)
356    }
357}