Skip to main content

rustapi/actions/
extrude_linedef.rs

1use crate::prelude::*;
2use rusterix::Surface;
3use vek::Vec3;
4
5pub struct ExtrudeLinedef {
6    id: TheId,
7    nodeui: TheNodeUI,
8}
9
10impl ExtrudeLinedef {
11    fn hash01(mut x: u32) -> f32 {
12        // Small deterministic hash for repeatable "random" profiles.
13        x ^= x >> 16;
14        x = x.wrapping_mul(0x7feb352d);
15        x ^= x >> 15;
16        x = x.wrapping_mul(0x846ca68b);
17        x ^= x >> 16;
18        (x as f32) / (u32::MAX as f32)
19    }
20
21    fn segment_height(style: i32, seg: u32, seg_count: u32, variation: f32, seed: u32) -> f32 {
22        if seg == 0 || seg + 1 == seg_count {
23            return 0.0;
24        }
25        match style {
26            // Crenelated: alternating high/low battlements.
27            1 => {
28                if seg % 2 == 0 {
29                    0.0
30                } else {
31                    variation
32                }
33            }
34            // Random/broken: irregular dips.
35            3 => variation * Self::hash01(seed ^ seg.wrapping_mul(1664525)),
36            _ => 0.0,
37        }
38    }
39
40    fn build_top_profile(
41        p1_top: Vec3<f32>,
42        p0_top: Vec3<f32>,
43        offset: Vec3<f32>,
44        style: i32,
45        segment_size: f32,
46        variation: f32,
47        seed: u32,
48    ) -> Vec<Vec3<f32>> {
49        let dir_vec = p0_top - p1_top;
50        let len = dir_vec.magnitude();
51        if len <= 1e-5 {
52            return vec![p1_top, p0_top];
53        }
54        if style == 0 {
55            return vec![p1_top, p0_top];
56        }
57
58        let dir = dir_vec / len;
59        let seg_size = segment_size.max(0.05);
60        let seg_count = ((len / seg_size).ceil() as u32).max(2);
61        let step = len / seg_count as f32;
62
63        let up = if offset.magnitude() > 1e-5 {
64            offset.normalized()
65        } else {
66            Vec3::new(0.0, 0.0, 1.0)
67        };
68        let down = -up;
69
70        let mut points = Vec::new();
71        points.push(p1_top);
72
73        if style == 2 {
74            // Palisade: triangular spikes.
75            for seg in 0..seg_count {
76                let start_t = seg as f32 * step;
77                let mid_t = start_t + step * 0.5;
78                let end_t = (seg + 1) as f32 * step;
79                let spike = variation.max(0.0);
80                points.push(p1_top + dir * mid_t + up * spike);
81                points.push(p1_top + dir * end_t);
82            }
83            return points;
84        }
85
86        // Crenelated/random: step profile with vertical jumps at segment boundaries.
87        let mut curr_h = Self::segment_height(style, 0, seg_count, variation.max(0.0), seed);
88        for b in 1..seg_count {
89            let t = b as f32 * step;
90            let boundary = p1_top + dir * t;
91            let next_h = Self::segment_height(style, b, seg_count, variation.max(0.0), seed);
92
93            points.push(boundary + down * curr_h);
94            if (next_h - curr_h).abs() > 1e-5 {
95                points.push(boundary + down * next_h);
96            }
97            curr_h = next_h;
98        }
99        points.push(p0_top + down * curr_h);
100        if curr_h > 1e-5 {
101            points.push(p0_top);
102        }
103
104        points
105    }
106
107    pub fn extrude_linedef(
108        &self,
109        map: &mut Map,
110        ld_id: u32,
111        distance: f32,
112        angle_deg: f32,
113        top_style: i32,
114        segment_size: f32,
115        top_variation: f32,
116    ) -> Option<u32> {
117        let ld = map.find_linedef(ld_id)?;
118        let v0 = ld.start_vertex;
119        let v1 = ld.end_vertex;
120
121        let p0v = map.find_vertex(v0)?;
122        let p1v = map.find_vertex(v1)?;
123        let p0 = Vec3::new(p0v.x, p0v.y, p0v.z);
124        let p1 = Vec3::new(p1v.x, p1v.y, p1v.z);
125
126        // Rotate around the linedef axis (its tangent) by `angle` degrees.
127        // Base direction is world +Z (map up). We first project it to be perpendicular to the axis
128        // so rotation never "slides" along the edge.
129        let axis = {
130            let mut a = p1 - p0; // linedef tangent
131            let len = a.magnitude();
132            if len > 1e-6 {
133                a /= len;
134            } else {
135                a = Vec3::new(1.0, 0.0, 0.0);
136            }
137            a
138        };
139        let line_len = (p1 - p0).magnitude();
140        // Avoid coplanar overlap at shared endpoints when adjacent linedefs are extruded.
141        // Strong trims are only needed on diagonal lines. On axis-aligned walls, large trims
142        // create visible corner gaps where two walls just meet.
143        let dx = (p1.x - p0.x).abs();
144        let dy = (p1.y - p0.y).abs();
145        let is_axis_aligned = dx < 1e-4 || dy < 1e-4;
146        let thickness = distance.abs();
147        let end_inset = if is_axis_aligned {
148            (thickness * 0.04)
149                .clamp(0.0, 0.01)
150                .min((line_len * 0.1).max(0.0))
151        } else {
152            (thickness * 0.35)
153                .clamp(0.02, 0.18)
154                .min((line_len * 0.25).max(0.0))
155        };
156        let p0_base = p0 + axis * end_inset;
157        let p1_base = p1 - axis * end_inset;
158
159        let mut base = Vec3::new(0.0, 0.0, 1.0); // world up (Z)
160        // Make base perpendicular to axis
161        base = base - axis * base.dot(axis);
162        let blen = base.magnitude();
163        if blen <= 1e-6 || !blen.is_finite() {
164            // If the edge is parallel to +Z, pick +X as base and reproject
165            base = Vec3::new(1.0, 0.0, 0.0) - axis * axis.dot(Vec3::new(1.0, 0.0, 0.0));
166        }
167        base = base.normalized();
168        let ortho = axis.cross(base); // also perpendicular to axis, 90° from base
169
170        let angle = angle_deg.to_radians();
171        let dir = base * angle.cos() - ortho * angle.sin();
172
173        let offset = dir * distance;
174        let p1_top = p1_base + offset;
175        let p0_top = p0_base + offset;
176
177        let top_points = Self::build_top_profile(
178            p1_top,
179            p0_top,
180            offset,
181            top_style,
182            segment_size,
183            top_variation,
184            ld_id,
185        );
186
187        // Build on duplicated base vertices so generated profile geometry never shares
188        // the original host linedef. This allows safe replace/delete of generated sectors.
189        let v0_base = map.add_vertex_at_3d(p0_base.x, p0_base.y, p0_base.z, false);
190        let v1_base = map.add_vertex_at_3d(p1_base.x, p1_base.y, p1_base.z, false);
191
192        // Use manual linedef creation to avoid premature sector detection
193        // (auto-detection can find wrong cycles when vertices are reused)
194        map.possible_polygon = vec![];
195        let _ = map.create_linedef_manual(v0_base, v1_base); // bottom
196        let mut prev = v1_base;
197        for p in top_points {
198            let v = map.add_vertex_at_3d(p.x, p.y, p.z, false);
199            let _ = map.create_linedef_manual(prev, v);
200            prev = v;
201        }
202        let _ = map.create_linedef_manual(prev, v0_base); // close side
203
204        map.close_polygon_manual()
205    }
206}
207
208impl Action for ExtrudeLinedef {
209    fn new() -> Self
210    where
211        Self: Sized,
212    {
213        let mut nodeui: TheNodeUI = TheNodeUI::default();
214
215        let item = TheNodeUIItem::FloatEditSlider(
216            "actionDistance".into(),
217            "".into(),
218            "".into(),
219            2.0,
220            0.0..=0.0,
221            false,
222        );
223        nodeui.add_item(item);
224
225        let item = TheNodeUIItem::FloatEditSlider(
226            "actionAngle".into(),
227            "".into(),
228            "".into(),
229            0.0,
230            0.0..=360.0,
231            false,
232        );
233        nodeui.add_item(item);
234        nodeui.add_item(TheNodeUIItem::OpenTree("top".into()));
235        nodeui.add_item(TheNodeUIItem::Selector(
236            "actionTopStyle".into(),
237            "".into(),
238            "".into(),
239            vec![
240                "flat".into(),
241                "crenelated".into(),
242                "palisade".into(),
243                "random".into(),
244            ],
245            0,
246        ));
247        nodeui.add_item(TheNodeUIItem::FloatEditSlider(
248            "actionTopSegmentSize".into(),
249            "".into(),
250            "".into(),
251            1.0,
252            0.1..=8.0,
253            false,
254        ));
255        nodeui.add_item(TheNodeUIItem::FloatEditSlider(
256            "actionTopVariation".into(),
257            "".into(),
258            "".into(),
259            0.5,
260            0.0..=4.0,
261            false,
262        ));
263        nodeui.add_item(TheNodeUIItem::CloseTree);
264
265        let item = TheNodeUIItem::Markdown("desc".into(), "".into());
266        nodeui.add_item(item);
267
268        Self {
269            id: TheId::named(&fl!("action_extrude_linedef")),
270            nodeui,
271        }
272    }
273
274    fn id(&self) -> TheId {
275        self.id.clone()
276    }
277
278    fn info(&self) -> String {
279        fl!("action_extrude_linedef_desc")
280    }
281
282    fn role(&self) -> ActionRole {
283        ActionRole::Editor
284    }
285
286    fn accel(&self) -> Option<TheAccelerator> {
287        Some(TheAccelerator::new(TheAcceleratorKey::ALT, 'e'))
288    }
289
290    fn is_applicable(&self, map: &Map, _ctx: &mut TheContext, server_ctx: &ServerContext) -> bool {
291        // 3D-only extrusion action.
292        if server_ctx.editor_view_mode == EditorViewMode::D2 {
293            return false;
294        }
295
296        map.selected_sectors.is_empty() && !map.selected_linedefs.is_empty()
297    }
298
299    fn apply(
300        &self,
301        map: &mut Map,
302        _ui: &mut TheUI,
303        _ctx: &mut TheContext,
304        server_ctx: &mut ServerContext,
305    ) -> Option<ProjectUndoAtom> {
306        let mut changed = false;
307        let prev = map.clone();
308
309        let distance = self.nodeui.get_f32_value("actionDistance").unwrap_or(2.0);
310        let angle = self.nodeui.get_f32_value("actionAngle").unwrap_or(0.0);
311        let top_style = self.nodeui.get_i32_value("actionTopStyle").unwrap_or(0);
312        let segment_size = self
313            .nodeui
314            .get_f32_value("actionTopSegmentSize")
315            .unwrap_or(1.0);
316        let top_variation = self
317            .nodeui
318            .get_f32_value("actionTopVariation")
319            .unwrap_or(0.5);
320
321        for linedef_id in &map.selected_linedefs.clone() {
322            if let Some(sector_id) = self.extrude_linedef(
323                map,
324                *linedef_id,
325                distance,
326                angle,
327                top_style,
328                segment_size,
329                top_variation,
330            ) {
331                let mut surface = Surface::new(sector_id);
332                surface.calculate_geometry(map);
333                map.surfaces.insert(surface.id, surface);
334                if let Some(sector) = map.find_sector_mut(sector_id) {
335                    sector
336                        .properties
337                        .set("generated_profile", Value::Bool(true));
338                    sector.properties.set(
339                        "generated_profile_host_linedef",
340                        Value::Int(*linedef_id as i32),
341                    );
342                }
343
344                changed = true;
345            }
346        }
347
348        if changed {
349            Some(ProjectUndoAtom::MapEdit(
350                server_ctx.pc,
351                Box::new(prev),
352                Box::new(map.clone()),
353            ))
354        } else {
355            None
356        }
357    }
358
359    fn params(&self) -> TheNodeUI {
360        self.nodeui.clone()
361    }
362
363    fn handle_event(
364        &mut self,
365        event: &TheEvent,
366        _project: &mut Project,
367        _ui: &mut TheUI,
368        _ctx: &mut TheContext,
369        _server_ctx: &mut ServerContext,
370    ) -> bool {
371        self.nodeui.handle_event(event)
372    }
373}