Skip to main content

rustapi/actions/
add_arch.rs

1use crate::prelude::*;
2
3pub struct AddArch {
4    id: TheId,
5    nodeui: TheNodeUI,
6}
7
8impl Action for AddArch {
9    fn new() -> Self
10    where
11        Self: Sized,
12    {
13        let mut nodeui: TheNodeUI = TheNodeUI::default();
14
15        let item = TheNodeUIItem::FloatEditSlider(
16            "actionArchHeight".into(),
17            "".into(),
18            "".into(),
19            1.0,
20            0.1..=2.0,
21            false,
22        );
23        nodeui.add_item(item);
24
25        let item = TheNodeUIItem::IntEditSlider(
26            "actionArchSegments".into(),
27            "".into(),
28            "".into(),
29            12,
30            4..=64,
31            false,
32        );
33        nodeui.add_item(item);
34
35        nodeui.add_item(TheNodeUIItem::Markdown("desc".into(), "".into()));
36
37        Self {
38            id: TheId::named(&fl!("action_add_arch")),
39            nodeui,
40        }
41    }
42
43    fn id(&self) -> TheId {
44        self.id.clone()
45    }
46
47    fn info(&self) -> String {
48        fl!("action_add_arch_desc")
49    }
50
51    fn role(&self) -> ActionRole {
52        ActionRole::Editor
53    }
54
55    fn accel(&self) -> Option<TheAccelerator> {
56        None
57    }
58
59    fn is_applicable(&self, map: &Map, _ctx: &mut TheContext, server_ctx: &ServerContext) -> bool {
60        !map.selected_linedefs.is_empty() && server_ctx.editor_view_mode == EditorViewMode::D2
61    }
62
63    fn apply(
64        &self,
65        map: &mut Map,
66        _ui: &mut TheUI,
67        _ctx: &mut TheContext,
68        server_ctx: &mut ServerContext,
69    ) -> Option<ProjectUndoAtom> {
70        if map.selected_linedefs.is_empty() {
71            return None;
72        }
73        let prev = map.clone();
74        let mut changed = false;
75
76        // read UI params
77        let height = self.nodeui.get_f32_value("actionArchHeight").unwrap_or(1.0) as f32;
78        let segments = self
79            .nodeui
80            .get_i32_value("actionArchSegments")
81            .unwrap_or(12)
82            .clamp(4, 64) as u32;
83
84        for ld_id in map.selected_linedefs.clone() {
85            if let Some(ld) = map.find_linedef(ld_id).cloned() {
86                // Remember original sector bindings and properties
87                let orig_sector_ids = ld.sector_ids.clone();
88                let orig_props = ld.properties.clone();
89
90                // Fetch endpoints in 3D (Y up)
91                if let (Some(a), Some(b)) = (
92                    map.find_vertex(ld.start_vertex).cloned(),
93                    map.find_vertex(ld.end_vertex).cloned(),
94                ) {
95                    // Work in XY plane only; do not displace Z
96                    let ax = a.x;
97                    let ay = a.y;
98                    let az = a.z;
99                    let bx = b.x;
100                    let by = b.y;
101                    let bz = b.z;
102
103                    // 2D tangent and perpendicular normal in XY
104                    let dx = bx - ax;
105                    let dy = by - ay;
106                    let mut nx = -dy;
107                    let mut ny = dx;
108                    let len = (nx * nx + ny * ny).sqrt();
109                    if len > 1e-6 {
110                        nx /= len;
111                        ny /= len;
112                    } else {
113                        nx = 0.0;
114                        ny = 1.0;
115                    }
116
117                    // Midpoint of the segment (in XY)
118                    let midx = (ax + bx) * 0.5;
119                    let midy = (ay + by) * 0.5;
120
121                    // --- Flip normal OUTWARD if we can infer an inside sector ---
122                    // Prefer a sector that is currently selected; otherwise use the first attached sector.
123                    let mut ref_sector_id: Option<u32> = None;
124                    if let Some(sel) = ld
125                        .sector_ids
126                        .iter()
127                        .find(|sid| map.selected_sectors.contains(sid))
128                    {
129                        ref_sector_id = Some(*sel);
130                    } else if let Some(first) = ld.sector_ids.first() {
131                        ref_sector_id = Some(*first);
132                    }
133
134                    if let Some(sec_id) = ref_sector_id {
135                        if let Some(sector) = map.find_sector(sec_id) {
136                            // Compute a lightweight centroid in XY from the sector's vertex loop
137                            let mut cx_sum = 0.0f32;
138                            let mut cy_sum = 0.0f32;
139                            let mut cnt = 0usize;
140                            for &edge_id in &sector.linedefs {
141                                if let Some(edge) = map.find_linedef(edge_id) {
142                                    if let Some(v) = map.find_vertex(edge.start_vertex) {
143                                        cx_sum += v.x;
144                                        cy_sum += v.y;
145                                        cnt += 1;
146                                    }
147                                }
148                            }
149                            if cnt > 0 {
150                                let scx = cx_sum / (cnt as f32);
151                                let scy = cy_sum / (cnt as f32);
152                                // Vector from mid to sector centroid
153                                let vx = scx - midx;
154                                let vy = scy - midy;
155                                // If normal points TOWARD the sector (dot > 0), flip to point outward
156                                if nx * vx + ny * vy > 0.0 {
157                                    nx = -nx;
158                                    ny = -ny;
159                                }
160                            }
161                        }
162                    }
163
164                    // Quadratic Bezier control point in XY (midpoint + n*height)
165                    let cx = midx + nx * height;
166                    let cy = midy + ny * height;
167
168                    // Create interior points along the Bezier in XY; Z is lerped only (no displacement)
169                    let step = 1.0_f32 / (segments as f32);
170                    let mut new_vertex_ids: Vec<u32> = Vec::new();
171                    for i in 1..segments {
172                        // interior only
173                        let t = step * (i as f32);
174                        let one_t = 1.0 - t;
175                        let px = ax * (one_t * one_t) + cx * (2.0 * one_t * t) + bx * (t * t);
176                        let py = ay * (one_t * one_t) + cy * (2.0 * one_t * t) + by * (t * t);
177                        let pz = az + (bz - az) * t; // maintain continuity; no Z bulge
178                        let vid = map.add_vertex_at_3d(px, py, pz, false);
179                        new_vertex_ids.push(vid);
180                    }
181
182                    // Build the new vertex chain including endpoints
183                    let mut chain: Vec<u32> = Vec::with_capacity(segments as usize + 1);
184                    chain.push(ld.start_vertex);
185                    chain.extend(new_vertex_ids.iter().copied());
186                    chain.push(ld.end_vertex);
187
188                    // Phase 1: create/reuse linedefs for each consecutive pair in the chain via Map API (no sector borrow yet)
189                    // Ensure standalone creation; don't chain with prior edges
190                    map.possible_polygon.clear();
191
192                    let mut new_ids: Vec<u32> = Vec::with_capacity(segments as usize);
193                    for w in chain.windows(2) {
194                        // Use manual creation to avoid unwanted sector auto-detection
195                        let new_ld_id = map.create_linedef_manual(w[0], w[1]);
196                        // Copy properties & bind sectors like the original linedef
197                        if let Some(nld) = map.find_linedef_mut(new_ld_id) {
198                            nld.properties = orig_props.clone();
199                            nld.sector_ids = orig_sector_ids.clone();
200                        }
201                        new_ids.push(new_ld_id);
202                    }
203                    // Clear possible_polygon since we don't want to create a sector
204                    map.possible_polygon.clear();
205
206                    // Phase 2: splice the new chain into every sector that referenced the old linedef
207                    let mut touched_sectors: Vec<u32> = Vec::new();
208                    for sector in map.sectors.iter_mut() {
209                        if let Some(pos) = sector.linedefs.iter().position(|&id| id == ld_id) {
210                            sector.linedefs.splice(pos..=pos, new_ids.iter().copied());
211                            if !touched_sectors.contains(&sector.id) {
212                                touched_sectors.push(sector.id);
213                            }
214                        }
215                    }
216
217                    // Update new linedefs with sector memberships
218                    let mut new_sector_ids = orig_sector_ids.clone();
219                    for sid in touched_sectors {
220                        if !new_sector_ids.contains(&sid) {
221                            new_sector_ids.push(sid);
222                        }
223                    }
224                    for nid in &new_ids {
225                        if let Some(nld) = map.find_linedef_mut(*nid) {
226                            nld.sector_ids = new_sector_ids.clone();
227                        }
228                    }
229
230                    // Update selection once (remove old id, add new chain) after sector updates
231                    if let Some(pos_sel) = map.selected_linedefs.iter().position(|&id| id == ld_id)
232                    {
233                        map.selected_linedefs.remove(pos_sel);
234                    }
235                    for nid in &new_ids {
236                        if !map.selected_linedefs.contains(nid) {
237                            map.selected_linedefs.push(*nid);
238                        }
239                    }
240
241                    // Remove old id from any sectors just in case
242                    for sector in map.sectors.iter_mut() {
243                        if let Some(pos) = sector.linedefs.iter().position(|&id| id == ld_id) {
244                            sector.linedefs.remove(pos);
245                        }
246                    }
247
248                    // Also drop the old linedef from the map's linedef list to avoid drawing it standalone
249                    map.linedefs.retain(|l| l.id != ld_id);
250
251                    changed = true;
252                }
253            }
254        }
255
256        if changed {
257            Some(ProjectUndoAtom::MapEdit(
258                server_ctx.pc,
259                Box::new(prev),
260                Box::new(map.clone()),
261            ))
262        } else {
263            None
264        }
265    }
266
267    fn params(&self) -> TheNodeUI {
268        self.nodeui.clone()
269    }
270
271    fn handle_event(
272        &mut self,
273        event: &TheEvent,
274        _project: &mut Project,
275        _ui: &mut TheUI,
276        _ctx: &mut TheContext,
277        _server_ctx: &mut ServerContext,
278    ) -> bool {
279        self.nodeui.handle_event(event)
280    }
281}