Skip to main content

rustapi/actions/
create_palisade.rs

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