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 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 1 => {
28 if seg % 2 == 0 {
29 0.0
30 } else {
31 variation
32 }
33 }
34 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 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 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 let axis = {
130 let mut a = p1 - p0; 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 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); base = base - axis * base.dot(axis);
162 let blen = base.magnitude();
163 if blen <= 1e-6 || !blen.is_finite() {
164 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); 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 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 map.possible_polygon = vec![];
195 let _ = map.create_linedef_manual(v0_base, v1_base); 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); 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 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}