1use crate::prelude::*;
2use rusterix::PixelSource;
3use std::str::FromStr;
4
5pub const CREATE_FENCE_ACTION_ID: &str = "1d4cc6df-66fd-4d28-a340-c702f696f7b8";
6
7pub struct CreateFence {
8 id: TheId,
9 nodeui: TheNodeUI,
10}
11
12impl CreateFence {
13 fn apply_linedef_feature(&self, map: &mut Map, linedef_id: u32) -> bool {
14 let spacing = self
15 .nodeui
16 .get_f32_value("actionLayoutSpacing")
17 .unwrap_or(1.5)
18 .max(0.1);
19 let post_size = self
20 .nodeui
21 .get_f32_value("actionPostSize")
22 .unwrap_or(0.18)
23 .max(0.02);
24 let post_shape = self.nodeui.get_i32_value("actionPostShape").unwrap_or(0);
25 let post_height = self.nodeui.get_f32_value("actionPostHeight").unwrap_or(2.0);
26 let round_segments = self
27 .nodeui
28 .get_i32_value("actionPostRoundSegments")
29 .unwrap_or(8)
30 .max(3);
31 let connector_count = self
32 .nodeui
33 .get_i32_value("actionConnectorCount")
34 .unwrap_or(2)
35 .max(0);
36 let connector_style = self
37 .nodeui
38 .get_i32_value("actionConnectorStyle")
39 .unwrap_or(0);
40 let connector_size = self
41 .nodeui
42 .get_f32_value("actionConnectorSize")
43 .unwrap_or(0.12)
44 .max(0.01);
45 let connector_drop = self
46 .nodeui
47 .get_f32_value("actionConnectorDrop")
48 .unwrap_or(1.2)
49 .max(0.0);
50 let lean_amount = self
51 .nodeui
52 .get_f32_value("actionLeanAmount")
53 .unwrap_or(0.0)
54 .max(0.0);
55 let lean_randomness = self
56 .nodeui
57 .get_f32_value("actionLeanRandomness")
58 .unwrap_or(1.0)
59 .clamp(0.0, 1.0);
60 let tile_id_text = self
61 .nodeui
62 .get_text_value("actionMaterialTileId")
63 .unwrap_or_default();
64 let tile_id = Uuid::parse_str(tile_id_text.trim()).ok();
65
66 let Some(linedef) = map.find_linedef_mut(linedef_id) else {
67 return false;
68 };
69
70 if post_height <= 0.0 {
71 linedef
72 .properties
73 .set("linedef_feature", Value::Str("None".to_string()));
74 return true;
75 }
76
77 linedef
78 .properties
79 .set("linedef_feature", Value::Str("Fence".to_string()));
80 linedef
81 .properties
82 .set("feature_layout_spacing", Value::Float(spacing));
83 linedef
84 .properties
85 .set("feature_post_size", Value::Float(post_size));
86 linedef
87 .properties
88 .set("feature_post_shape", Value::Int(post_shape));
89 linedef
90 .properties
91 .set("feature_height", Value::Float(post_height));
92 linedef
93 .properties
94 .set("feature_round_segments", Value::Int(round_segments));
95 linedef
96 .properties
97 .set("feature_connector_count", Value::Int(connector_count));
98 linedef
99 .properties
100 .set("feature_connector_style", Value::Int(connector_style));
101 linedef
102 .properties
103 .set("feature_connector_size", Value::Float(connector_size));
104 linedef
105 .properties
106 .set("feature_connector_drop", Value::Float(connector_drop));
107 linedef
108 .properties
109 .set("feature_lean_amount", Value::Float(lean_amount));
110 linedef
111 .properties
112 .set("feature_lean_randomness", Value::Float(lean_randomness));
113
114 if let Some(id) = tile_id {
115 linedef
116 .properties
117 .set("feature_source", Value::Source(PixelSource::TileId(id)));
118 } else {
119 linedef.properties.remove("feature_source");
120 }
121
122 true
123 }
124}
125
126impl Action for CreateFence {
127 fn new() -> Self
128 where
129 Self: Sized,
130 {
131 let mut nodeui = TheNodeUI::default();
132
133 nodeui.add_item(TheNodeUIItem::OpenTree("material".into()));
134 nodeui.add_item(TheNodeUIItem::Text(
135 "actionMaterialTileId".into(),
136 "".into(),
137 "".into(),
138 "".into(),
139 None,
140 false,
141 ));
142 nodeui.add_item(TheNodeUIItem::CloseTree);
143
144 nodeui.add_item(TheNodeUIItem::OpenTree("layout".into()));
145 nodeui.add_item(TheNodeUIItem::FloatEditSlider(
146 "actionLayoutSpacing".into(),
147 "".into(),
148 "".into(),
149 1.5,
150 0.1..=8.0,
151 false,
152 ));
153 nodeui.add_item(TheNodeUIItem::CloseTree);
154
155 nodeui.add_item(TheNodeUIItem::OpenTree("posts".into()));
156 nodeui.add_item(TheNodeUIItem::Selector(
157 "actionPostShape".into(),
158 "".into(),
159 "".into(),
160 vec!["square".into(), "round".into()],
161 0,
162 ));
163 nodeui.add_item(TheNodeUIItem::FloatEditSlider(
164 "actionPostSize".into(),
165 "".into(),
166 "".into(),
167 0.18,
168 0.02..=2.0,
169 false,
170 ));
171 nodeui.add_item(TheNodeUIItem::FloatEditSlider(
172 "actionPostHeight".into(),
173 "".into(),
174 "".into(),
175 2.0,
176 0.0..=8.0,
177 false,
178 ));
179 nodeui.add_item(TheNodeUIItem::IntEditSlider(
180 "actionPostRoundSegments".into(),
181 "".into(),
182 "".into(),
183 8,
184 3..=24,
185 false,
186 ));
187 nodeui.add_item(TheNodeUIItem::CloseTree);
188
189 nodeui.add_item(TheNodeUIItem::OpenTree("connectors".into()));
190 nodeui.add_item(TheNodeUIItem::IntEditSlider(
191 "actionConnectorCount".into(),
192 "".into(),
193 "".into(),
194 2,
195 0..=8,
196 false,
197 ));
198 nodeui.add_item(TheNodeUIItem::Selector(
199 "actionConnectorStyle".into(),
200 "".into(),
201 "".into(),
202 vec!["plank".into(), "square".into(), "round".into()],
203 0,
204 ));
205 nodeui.add_item(TheNodeUIItem::FloatEditSlider(
206 "actionConnectorSize".into(),
207 "".into(),
208 "".into(),
209 0.12,
210 0.01..=1.0,
211 false,
212 ));
213 nodeui.add_item(TheNodeUIItem::FloatEditSlider(
214 "actionConnectorDrop".into(),
215 "".into(),
216 "".into(),
217 1.2,
218 0.0..=6.0,
219 false,
220 ));
221 nodeui.add_item(TheNodeUIItem::CloseTree);
222
223 nodeui.add_item(TheNodeUIItem::OpenTree("lean".into()));
224 nodeui.add_item(TheNodeUIItem::FloatEditSlider(
225 "actionLeanAmount".into(),
226 "".into(),
227 "".into(),
228 0.0,
229 0.0..=1.5,
230 false,
231 ));
232 nodeui.add_item(TheNodeUIItem::FloatEditSlider(
233 "actionLeanRandomness".into(),
234 "".into(),
235 "".into(),
236 1.0,
237 0.0..=1.0,
238 false,
239 ));
240 nodeui.add_item(TheNodeUIItem::CloseTree);
241
242 nodeui.add_item(TheNodeUIItem::Markdown("desc".into(), "".into()));
243
244 Self {
245 id: TheId::named_with_id(
246 "Create Fence",
247 Uuid::from_str(CREATE_FENCE_ACTION_ID).unwrap(),
248 ),
249 nodeui,
250 }
251 }
252
253 fn id(&self) -> TheId {
254 self.id.clone()
255 }
256
257 fn info(&self) -> String {
258 "Configure a non-destructive fence feature on selected linedefs.".to_string()
259 }
260
261 fn role(&self) -> ActionRole {
262 ActionRole::Editor
263 }
264
265 fn is_applicable(&self, map: &Map, _ctx: &mut TheContext, server_ctx: &ServerContext) -> bool {
266 if server_ctx.editor_view_mode == EditorViewMode::D2 {
267 return false;
268 }
269 map.selected_sectors.is_empty() && !map.selected_linedefs.is_empty()
270 }
271
272 fn load_params(&mut self, map: &Map) {
273 let Some(linedef_id) = map.selected_linedefs.first() else {
274 return;
275 };
276 let Some(linedef) = map.find_linedef(*linedef_id) else {
277 return;
278 };
279
280 self.nodeui.set_f32_value(
281 "actionLayoutSpacing",
282 linedef
283 .properties
284 .get_float_default("feature_layout_spacing", 1.5),
285 );
286 self.nodeui.set_i32_value(
287 "actionPostShape",
288 linedef.properties.get_int_default("feature_post_shape", 0),
289 );
290 self.nodeui.set_f32_value(
291 "actionPostSize",
292 linedef
293 .properties
294 .get_float_default("feature_post_size", 0.18),
295 );
296 self.nodeui.set_f32_value(
297 "actionPostHeight",
298 linedef.properties.get_float_default("feature_height", 2.0),
299 );
300 self.nodeui.set_i32_value(
301 "actionPostRoundSegments",
302 linedef
303 .properties
304 .get_int_default("feature_round_segments", 8),
305 );
306 self.nodeui.set_i32_value(
307 "actionConnectorCount",
308 linedef
309 .properties
310 .get_int_default("feature_connector_count", 2),
311 );
312 self.nodeui.set_i32_value(
313 "actionConnectorStyle",
314 linedef
315 .properties
316 .get_int_default("feature_connector_style", 0),
317 );
318 self.nodeui.set_f32_value(
319 "actionConnectorSize",
320 linedef
321 .properties
322 .get_float_default("feature_connector_size", 0.12),
323 );
324 self.nodeui.set_f32_value(
325 "actionConnectorDrop",
326 linedef
327 .properties
328 .get_float_default("feature_connector_drop", 1.2),
329 );
330 self.nodeui.set_f32_value(
331 "actionLeanAmount",
332 linedef
333 .properties
334 .get_float_default("feature_lean_amount", 0.0),
335 );
336 self.nodeui.set_f32_value(
337 "actionLeanRandomness",
338 linedef
339 .properties
340 .get_float_default("feature_lean_randomness", 1.0),
341 );
342
343 let tile_id_text = match linedef.properties.get("feature_source") {
344 Some(Value::Source(PixelSource::TileId(id))) => id.to_string(),
345 _ => String::new(),
346 };
347 self.nodeui
348 .set_text_value("actionMaterialTileId", tile_id_text);
349 }
350
351 fn apply(
352 &self,
353 map: &mut Map,
354 _ui: &mut TheUI,
355 _ctx: &mut TheContext,
356 server_ctx: &mut ServerContext,
357 ) -> Option<ProjectUndoAtom> {
358 let prev = map.clone();
359 let mut changed = false;
360
361 for linedef_id in map.selected_linedefs.clone() {
362 changed |= self.apply_linedef_feature(map, linedef_id);
363 }
364
365 if changed {
366 Some(ProjectUndoAtom::MapEdit(
367 server_ctx.pc,
368 Box::new(prev),
369 Box::new(map.clone()),
370 ))
371 } else {
372 None
373 }
374 }
375
376 fn params(&self) -> TheNodeUI {
377 self.nodeui.clone()
378 }
379
380 fn handle_event(
381 &mut self,
382 event: &TheEvent,
383 _project: &mut Project,
384 _ui: &mut TheUI,
385 _ctx: &mut TheContext,
386 _server_ctx: &mut ServerContext,
387 ) -> bool {
388 self.nodeui.handle_event(event)
389 }
390}