1use crate::prelude::*;
2use rusterix::{PixelSource, Value};
3use std::str::FromStr;
4
5pub const EDIT_VERTEX_ACTION_ID: &str = "260fcd81-c456-4ad4-894c-85e7552c856f";
6
7pub struct EditVertex {
8 id: TheId,
9 nodeui: TheNodeUI,
10 show_terrain: bool,
11}
12
13impl EditVertex {
14 fn build_nodeui(show_terrain: bool) -> TheNodeUI {
15 let mut nodeui: TheNodeUI = TheNodeUI::default();
16
17 let item = TheNodeUIItem::Text(
18 "actionVertexName".into(),
19 "".into(),
20 "".into(),
21 "".into(),
22 None,
23 false,
24 );
25 nodeui.add_item(item);
26
27 let item = TheNodeUIItem::FloatEditSlider(
28 "actionVertexX".into(),
29 "".into(),
30 "".into(),
31 0.0,
32 0.0..=0.0,
33 false,
34 );
35 nodeui.add_item(item);
36
37 let item = TheNodeUIItem::FloatEditSlider(
38 "actionVertexY".into(),
39 "".into(),
40 "".into(),
41 0.0,
42 0.0..=0.0,
43 false,
44 );
45 nodeui.add_item(item);
46
47 let item = TheNodeUIItem::FloatEditSlider(
48 "actionVertexZ".into(),
49 "".into(),
50 "".into(),
51 0.0,
52 0.0..=0.0,
53 false,
54 );
55 nodeui.add_item(item);
56
57 if show_terrain {
58 nodeui.add_item(TheNodeUIItem::OpenTree("terrain".into()));
59 nodeui.add_item(TheNodeUIItem::Checkbox(
60 "actionTerrain".into(),
61 "".into(),
62 "".into(),
63 false,
64 ));
65
66 let item = TheNodeUIItem::FloatEditSlider(
67 "actionTerrainSmoothness".into(),
68 "".into(),
69 "".into(),
70 0.0,
71 0.0..=0.0,
72 false,
73 );
74 nodeui.add_item(item);
75
76 nodeui.add_item(TheNodeUIItem::Icons(
77 "actionTerrainTile".into(),
78 "".into(),
79 "".into(),
80 vec![(
81 TheRGBABuffer::new(TheDim::sized(36, 36)),
82 "".to_string(),
83 Uuid::nil(),
84 )],
85 ));
86
87 nodeui.add_item(TheNodeUIItem::Text(
88 "actionTerrainTileId".into(),
89 "".into(),
90 "".into(),
91 "".into(),
92 None,
93 false,
94 ));
95
96 nodeui.add_item(TheNodeUIItem::FloatEditSlider(
97 "actionTerrainTileFalloff".into(),
98 "".into(),
99 "".into(),
100 1.0,
101 0.0..=16.0,
102 false,
103 ));
104 nodeui.add_item(TheNodeUIItem::CloseTree);
105 }
106
107 nodeui.add_item(TheNodeUIItem::OpenTree("billboard".into()));
108 nodeui.add_item(TheNodeUIItem::Icons(
109 "actionTile".into(),
110 "".into(),
111 "".into(),
112 vec![(
113 TheRGBABuffer::new(TheDim::sized(36, 36)),
114 "".to_string(),
115 Uuid::nil(),
116 )],
117 ));
118 nodeui.add_item(TheNodeUIItem::Text(
119 "actionTileId".into(),
120 "".into(),
121 "".into(),
122 "".into(),
123 None,
124 false,
125 ));
126 nodeui.add_item(TheNodeUIItem::FloatEditSlider(
127 "actionSize".into(),
128 "".into(),
129 "".into(),
130 1.0,
131 0.0..=0.0,
132 false,
133 ));
134 nodeui.add_item(TheNodeUIItem::CloseTree);
135
136 nodeui
137 }
138}
139
140impl Action for EditVertex {
141 fn new() -> Self
142 where
143 Self: Sized,
144 {
145 let nodeui = Self::build_nodeui(true);
146
147 Self {
151 id: TheId::named_with_id(
152 &fl!("action_edit_vertex"),
153 Uuid::from_str(EDIT_VERTEX_ACTION_ID).unwrap(),
154 ),
155 nodeui,
156 show_terrain: true,
157 }
158 }
159
160 fn id(&self) -> TheId {
161 self.id.clone()
162 }
163
164 fn info(&self) -> String {
165 fl!("action_edit_vertex_desc")
166 }
167
168 fn role(&self) -> ActionRole {
169 ActionRole::Editor
170 }
171
172 fn accel(&self) -> Option<TheAccelerator> {
173 None
174 }
175
176 fn is_applicable(&self, map: &Map, _ctx: &mut TheContext, _server_ctx: &ServerContext) -> bool {
177 map.selected_vertices.len() == 1
178 }
179
180 fn load_params(&mut self, map: &Map) {
181 if let Some(vertex_id) = map.selected_vertices.first() {
182 if let Some(vertex) = map.find_vertex(*vertex_id) {
183 self.nodeui
184 .set_text_value("actionVertexName", vertex.name.clone());
185 self.nodeui.set_bool_value(
186 "actionTerrain",
187 vertex.properties.get_bool_default("terrain_control", false),
188 );
189 self.nodeui.set_f32_value(
190 "actionTerrainSmoothness",
191 vertex.properties.get_float_default("smoothness", 1.0),
192 );
193 self.nodeui.set_f32_value(
194 "actionTerrainTileFalloff",
195 vertex
196 .properties
197 .get_float_default("terrain_tile_falloff", 1.0),
198 );
199 let terrain_tile_id = if let Some(Value::Source(PixelSource::TileId(id))) =
200 vertex.properties.get("terrain_source")
201 {
202 *id
203 } else {
204 Uuid::nil()
205 };
206 self.nodeui.set_text_value(
207 "actionTerrainTileId",
208 if terrain_tile_id == Uuid::nil() {
209 String::new()
210 } else {
211 terrain_tile_id.to_string()
212 },
213 );
214 if let Some(item) = self.nodeui.get_item_mut("actionTerrainTile")
215 && let TheNodeUIItem::Icons(_, _, _, items) = item
216 && items.len() == 1
217 {
218 items[0].2 = terrain_tile_id;
219 }
220
221 self.nodeui.set_f32_value(
222 "actionSize",
223 vertex.properties.get_float_default("source_size", 1.0),
224 );
225 let billboard_tile_id = if let Some(Value::Source(PixelSource::TileId(id))) =
226 vertex.properties.get("source")
227 {
228 *id
229 } else {
230 Uuid::nil()
231 };
232 self.nodeui.set_text_value(
233 "actionTileId",
234 if billboard_tile_id == Uuid::nil() {
235 String::new()
236 } else {
237 billboard_tile_id.to_string()
238 },
239 );
240 if let Some(item) = self.nodeui.get_item_mut("actionTile")
241 && let TheNodeUIItem::Icons(_, _, _, items) = item
242 && items.len() == 1
243 {
244 items[0].2 = billboard_tile_id;
245 }
246
247 self.nodeui.set_f32_value("actionVertexX", vertex.x);
248 self.nodeui.set_f32_value("actionVertexY", vertex.z);
249 self.nodeui.set_f32_value("actionVertexZ", vertex.y);
250 }
251 }
252 }
253
254 fn load_params_project(&mut self, project: &Project, server_ctx: &mut ServerContext) {
255 let show_terrain = server_ctx.get_map_context() == MapContext::Region;
256 if show_terrain != self.show_terrain {
257 let name = self
258 .nodeui
259 .get_text_value("actionVertexName")
260 .unwrap_or_default();
261 let x = self.nodeui.get_f32_value("actionVertexX").unwrap_or(0.0);
262 let y = self.nodeui.get_f32_value("actionVertexY").unwrap_or(0.0);
263 let z = self.nodeui.get_f32_value("actionVertexZ").unwrap_or(0.0);
264 let tile_size = self.nodeui.get_f32_value("actionSize").unwrap_or(1.0);
265 let tile_id_text = self
266 .nodeui
267 .get_text_value("actionTileId")
268 .unwrap_or_default();
269 let tile_id = self
270 .nodeui
271 .get_tile_id("actionTile", 0)
272 .unwrap_or(Uuid::nil());
273 let terrain_control = self.nodeui.get_bool_value("actionTerrain").unwrap_or(false);
274 let terrain_smoothness = self
275 .nodeui
276 .get_f32_value("actionTerrainSmoothness")
277 .unwrap_or(1.0);
278 let terrain_tile_falloff = self
279 .nodeui
280 .get_f32_value("actionTerrainTileFalloff")
281 .unwrap_or(1.0);
282 let terrain_tile_id_text = self
283 .nodeui
284 .get_text_value("actionTerrainTileId")
285 .unwrap_or_default();
286 let terrain_tile_id = self
287 .nodeui
288 .get_tile_id("actionTerrainTile", 0)
289 .unwrap_or(Uuid::nil());
290
291 self.nodeui = Self::build_nodeui(show_terrain);
292 self.show_terrain = show_terrain;
293
294 self.nodeui.set_text_value("actionVertexName", name);
295 self.nodeui.set_f32_value("actionVertexX", x);
296 self.nodeui.set_f32_value("actionVertexY", y);
297 self.nodeui.set_f32_value("actionVertexZ", z);
298 self.nodeui.set_f32_value("actionSize", tile_size);
299 self.nodeui.set_text_value("actionTileId", tile_id_text);
300 if let Some(item) = self.nodeui.get_item_mut("actionTile")
301 && let TheNodeUIItem::Icons(_, _, _, items) = item
302 && items.len() == 1
303 {
304 items[0].2 = tile_id;
305 }
306 self.nodeui.set_bool_value("actionTerrain", terrain_control);
307 self.nodeui
308 .set_f32_value("actionTerrainSmoothness", terrain_smoothness);
309 self.nodeui
310 .set_f32_value("actionTerrainTileFalloff", terrain_tile_falloff);
311 self.nodeui
312 .set_text_value("actionTerrainTileId", terrain_tile_id_text);
313 if let Some(item) = self.nodeui.get_item_mut("actionTerrainTile")
314 && let TheNodeUIItem::Icons(_, _, _, items) = item
315 && items.len() == 1
316 {
317 items[0].2 = terrain_tile_id;
318 }
319 }
320
321 let mut tile_icon = TheRGBABuffer::new(TheDim::sized(36, 36));
322 let mut tile_id = Uuid::nil();
323 let mut terrain_tile_icon = TheRGBABuffer::new(TheDim::sized(36, 36));
324 let mut terrain_tile_id = Uuid::nil();
325
326 if let Some(map) = project.get_map(server_ctx) {
327 if let Some(vertex_id) = map.selected_vertices.first() {
328 if let Some(vertex) = map.find_vertex(*vertex_id) {
329 if let Some(Value::Source(PixelSource::TileId(id))) =
330 vertex.properties.get("source")
331 {
332 if let Some(tile) = project.tiles.get(id)
333 && !tile.is_empty()
334 {
335 tile_icon = tile.textures[0].to_rgba();
336 tile_id = *id;
337 }
338 }
339 if let Some(Value::Source(PixelSource::TileId(id))) =
340 vertex.properties.get("terrain_source")
341 {
342 if let Some(tile) = project.tiles.get(id)
343 && !tile.is_empty()
344 {
345 terrain_tile_icon = tile.textures[0].to_rgba();
346 terrain_tile_id = *id;
347 }
348 }
349 }
350 }
351 }
352
353 if let Some(item) = self.nodeui.get_item_mut("actionTile") {
354 match item {
355 TheNodeUIItem::Icons(_, _, _, items) => {
356 if items.len() == 1 {
357 items[0].0 = tile_icon;
358 items[0].2 = tile_id;
359 }
360 }
361 _ => {}
362 }
363 }
364
365 if let Some(item) = self.nodeui.get_item_mut("actionTerrainTile") {
366 match item {
367 TheNodeUIItem::Icons(_, _, _, items) => {
368 if items.len() == 1 {
369 items[0].0 = terrain_tile_icon;
370 items[0].2 = terrain_tile_id;
371 }
372 }
373 _ => {}
374 }
375 }
376 }
377
378 fn apply(
379 &self,
380 map: &mut Map,
381 _ui: &mut TheUI,
382 _ctx: &mut TheContext,
383 server_ctx: &mut ServerContext,
384 ) -> Option<ProjectUndoAtom> {
385 let mut changed = false;
386 let prev = map.clone();
387
388 let name = self
389 .nodeui
390 .get_text_value("actionVertexName")
391 .unwrap_or(String::new());
392
393 let terrain_control = self.nodeui.get_bool_value("actionTerrain").unwrap_or(false);
394
395 let terrain_smoothness = self
396 .nodeui
397 .get_f32_value("actionTerrainSmoothness")
398 .unwrap_or(1.0);
399 let terrain_tile_falloff = self
400 .nodeui
401 .get_f32_value("actionTerrainTileFalloff")
402 .unwrap_or(1.0);
403 let terrain_tile_id = self
404 .nodeui
405 .get_tile_id("actionTerrainTile", 0)
406 .unwrap_or(Uuid::nil());
407 let terrain_tile_id_text = self
408 .nodeui
409 .get_text_value("actionTerrainTileId")
410 .unwrap_or_default();
411 let terrain_tile_id = if let Ok(id) = Uuid::parse_str(terrain_tile_id_text.trim()) {
412 id
413 } else {
414 terrain_tile_id
415 };
416 let is_region_mode = server_ctx.get_map_context() == MapContext::Region;
417
418 let tile_size = self.nodeui.get_f32_value("actionSize").unwrap_or(1.0);
419
420 let tile_id = self
421 .nodeui
422 .get_tile_id("actionTile", 0)
423 .unwrap_or(Uuid::nil());
424 let tile_id_text = self
425 .nodeui
426 .get_text_value("actionTileId")
427 .unwrap_or_default();
428 let tile_id = if let Ok(id) = Uuid::parse_str(tile_id_text.trim()) {
429 id
430 } else {
431 tile_id
432 };
433
434 let x = self.nodeui.get_f32_value("actionVertexX").unwrap_or(0.0);
435 let y = self.nodeui.get_f32_value("actionVertexY").unwrap_or(0.0);
436 let z = self.nodeui.get_f32_value("actionVertexZ").unwrap_or(0.0);
437
438 if let Some(vertex_id) = map.selected_vertices.first() {
439 if let Some(vertex) = map.find_vertex_mut(*vertex_id) {
440 if is_region_mode {
441 let ex_terrain_control =
442 vertex.properties.get_bool_default("terrain_control", false);
443
444 if ex_terrain_control != terrain_control {
445 vertex
446 .properties
447 .set("terrain_control", Value::Bool(terrain_control));
448 changed = true;
449 }
450
451 let ex_terrain_smoothness =
452 vertex.properties.get_float_default("smoothness", 1.0);
453 if ex_terrain_smoothness != terrain_smoothness {
454 vertex
455 .properties
456 .set("smoothness", Value::Float(terrain_smoothness));
457 changed = true;
458 }
459
460 let ex_terrain_tile_falloff = vertex
461 .properties
462 .get_float_default("terrain_tile_falloff", 1.0);
463 if (ex_terrain_tile_falloff - terrain_tile_falloff).abs() > f32::EPSILON {
464 vertex.properties.set(
465 "terrain_tile_falloff",
466 Value::Float(terrain_tile_falloff.max(0.0)),
467 );
468 changed = true;
469 }
470
471 match terrain_tile_id {
472 id if id != Uuid::nil() => {
473 let has_changed = match vertex.properties.get("terrain_source") {
474 Some(Value::Source(PixelSource::TileId(existing))) => {
475 *existing != id
476 }
477 _ => true,
478 };
479 if has_changed {
480 vertex
481 .properties
482 .set("terrain_source", Value::Source(PixelSource::TileId(id)));
483 changed = true;
484 }
485 }
486 _ => {
487 if vertex.properties.contains("terrain_source") {
488 vertex.properties.remove("terrain_source");
489 changed = true;
490 }
491 }
492 }
493 }
494
495 let ex_tile_size = vertex.properties.get_float_default("source_size", 1.0);
496 if ex_tile_size != tile_size {
497 vertex
498 .properties
499 .set("source_size", Value::Float(tile_size));
500 changed = true;
501 }
502
503 if tile_id != Uuid::nil() {
504 let has_changed = match vertex.properties.get("source") {
505 Some(Value::Source(PixelSource::TileId(existing))) => *existing != tile_id,
506 _ => true,
507 };
508 if has_changed {
509 vertex.properties.set(
510 "source",
511 Value::Source(rusterix::PixelSource::TileId(tile_id)),
512 );
513 changed = true;
514 }
515 } else if vertex.properties.contains("source") {
516 vertex.properties.remove("source");
517 changed = true;
518 }
519
520 if name != vertex.name {
521 vertex.name = name;
522 changed = true;
523 }
524 if x != vertex.x {
525 vertex.x = x;
526 changed = true;
527 }
528 if y != vertex.z {
530 vertex.z = y;
531 changed = true;
532 }
533 if z != vertex.y {
534 vertex.y = z;
535 changed = true;
536 }
537 }
538 }
539
540 if changed {
541 Some(ProjectUndoAtom::MapEdit(
542 server_ctx.pc,
543 Box::new(prev),
544 Box::new(map.clone()),
545 ))
546 } else {
547 None
548 }
549 }
550
551 fn params(&self) -> TheNodeUI {
552 self.nodeui.clone()
553 }
554
555 fn handle_event(
556 &mut self,
557 event: &TheEvent,
558 project: &mut Project,
559 _ui: &mut TheUI,
560 ctx: &mut TheContext,
561 _server_ctx: &mut ServerContext,
562 ) -> bool {
563 if let TheEvent::TileDropped(id, tile_id, index) = event {
564 if let Some(item) = self.nodeui.get_item_mut(&id.name) {
565 match item {
566 TheNodeUIItem::Icons(_, _, _, items) => {
567 if *index < items.len() {
568 if let Some(tile) = project.tiles.get(tile_id)
569 && !tile.is_empty()
570 {
571 items[*index].0 = tile.textures[0].to_rgba();
572 items[*index].2 = *tile_id;
573 if id.name == "actionTile" {
574 self.nodeui
575 .set_text_value("actionTileId", tile_id.to_string());
576 }
577 if id.name == "actionTerrainTile" {
578 self.nodeui
579 .set_text_value("actionTerrainTileId", tile_id.to_string());
580 }
581 ctx.ui.send(TheEvent::Custom(
582 TheId::named("Update Action List"),
583 TheValue::Empty,
584 ));
585 return true;
586 }
587 }
588 }
589 _ => {}
590 }
591 }
592 }
593 self.nodeui.handle_event(event)
594 }
595}