1pub use crate::prelude::*;
2
3pub mod add_arch;
4pub mod apply_tile;
5pub mod clear_palette;
6pub mod clear_profile;
7pub mod clear_tile;
8pub mod copy_tile_id;
9pub mod copy_vcode;
10pub mod create_campfire;
11pub mod create_center_vertex;
12pub mod create_fence;
13pub mod create_linedef;
14pub mod create_palisade;
15pub mod create_prop;
16pub mod create_roof;
17pub mod create_sector;
18pub mod create_stairs;
19pub mod duplicate;
20pub mod duplicate_tile;
21pub mod edit_linedef;
22pub mod edit_maximize;
23pub mod edit_sector;
24pub mod edit_tile_meta;
25pub mod edit_vertex;
26pub mod editing_camera;
27pub mod editing_slice;
28pub mod export_vcode;
29pub mod extrude_linedef;
30pub mod extrude_sector;
31pub mod firstp_camera;
32pub mod gate_door;
33pub mod import_palette;
34pub mod import_vcode;
35pub mod iso_camera;
36pub mod minimize;
37pub mod new_tile;
38pub mod orbit_camera;
39pub mod paste_vcode;
40pub mod recess;
41pub mod relief;
42pub mod remap_tile;
43pub mod set_editing_surface;
44pub mod set_tile_material;
45pub mod split;
46pub mod toggle_editing_geo;
47pub mod toggle_rect_geo;
48pub mod window;
49
50#[derive(PartialEq)]
51pub enum ActionRole {
52 Camera,
53 Editor,
54 Dock,
55}
56
57impl ActionRole {
58 pub fn to_color(&self) -> [u8; 4] {
59 match self {
60 ActionRole::Camera => [160, 175, 190, 255],
61 ActionRole::Editor => [195, 170, 150, 255],
62 ActionRole::Dock => [200, 195, 150, 255],
63 }
65 }
66}
67
68#[allow(unused)]
69pub trait Action: Send + Sync {
70 fn new() -> Self
71 where
72 Self: Sized;
73
74 fn id(&self) -> TheId;
75 fn info(&self) -> String;
76 fn role(&self) -> ActionRole;
77
78 fn accel(&self) -> Option<TheAccelerator> {
79 None
80 }
81
82 fn is_applicable(&self, map: &Map, ctx: &mut TheContext, server_ctx: &ServerContext) -> bool;
83
84 fn load_params(&mut self, map: &Map) {}
85 fn load_params_project(&mut self, project: &Project, server_ctx: &mut ServerContext) {}
86
87 fn apply(
88 &self,
89 map: &mut Map,
90 ui: &mut TheUI,
91 ctx: &mut TheContext,
92 server_ctx: &mut ServerContext,
93 ) -> Option<ProjectUndoAtom> {
94 None
95 }
96
97 fn apply_project(
98 &self,
99 project: &mut Project,
100 ui: &mut TheUI,
101 ctx: &mut TheContext,
102 server_ctx: &mut ServerContext,
103 ) {
104 }
105
106 fn params(&self) -> TheNodeUI;
107
108 fn handle_event(
109 &mut self,
110 event: &TheEvent,
111 project: &mut Project,
112 ui: &mut TheUI,
113 ctx: &mut TheContext,
114 server_ctx: &mut ServerContext,
115 ) -> bool;
116}
117
118fn normalize_toml_key(key: &str) -> String {
119 let mut out = String::new();
120 let mut prev_is_sep = false;
121
122 for (i, ch) in key.chars().enumerate() {
123 if ch.is_ascii_alphanumeric() {
124 if ch.is_ascii_uppercase() {
125 if i > 0 && !prev_is_sep {
126 out.push('_');
127 }
128 out.push(ch.to_ascii_lowercase());
129 } else {
130 out.push(ch.to_ascii_lowercase());
131 }
132 prev_is_sep = false;
133 } else if !prev_is_sep && !out.is_empty() {
134 out.push('_');
135 prev_is_sep = true;
136 }
137 }
138
139 out.trim_matches('_').to_string()
140}
141
142fn action_param_key(id: &str) -> String {
143 let key = normalize_toml_key(id);
144 key.strip_prefix("action_").unwrap_or(&key).to_string()
145}
146
147fn round_f64_3(v: f64) -> f64 {
148 (v * 1000.0).round() / 1000.0
149}
150
151fn root_table_prefix(nodeui: &TheNodeUI) -> Option<String> {
152 let mut section_stack: Vec<String> = vec![];
153 let mut prefix: Option<String> = None;
154 let mut saw = false;
155
156 for (_, item) in nodeui.list_items() {
157 match item {
158 TheNodeUIItem::OpenTree(name) => section_stack.push(normalize_toml_key(name)),
159 TheNodeUIItem::CloseTree => {
160 section_stack.pop();
161 }
162 TheNodeUIItem::Text(id, _, _, _, _, _)
163 | TheNodeUIItem::Selector(id, _, _, _, _)
164 | TheNodeUIItem::FloatEditSlider(id, _, _, _, _, _)
165 | TheNodeUIItem::FloatSlider(id, _, _, _, _, _, _)
166 | TheNodeUIItem::IntEditSlider(id, _, _, _, _, _)
167 | TheNodeUIItem::PaletteSlider(id, _, _, _, _, _)
168 | TheNodeUIItem::IntSlider(id, _, _, _, _, _, _)
169 | TheNodeUIItem::ColorPicker(id, _, _, _, _)
170 | TheNodeUIItem::Checkbox(id, _, _, _) => {
171 if section_stack.is_empty() {
172 let key = action_param_key(id);
173 if let Some((p, _)) = key.split_once('_') {
174 let p = p.to_string();
175 match &prefix {
176 None => prefix = Some(p),
177 Some(curr) if curr == &p => {}
178 Some(_) => return None,
179 }
180 saw = true;
181 } else {
182 return None;
183 }
184 }
185 }
186 TheNodeUIItem::Button(_, _, _, _)
187 | TheNodeUIItem::Markdown(_, _)
188 | TheNodeUIItem::Separator(_)
189 | TheNodeUIItem::Icons(_, _, _, _) => {}
190 }
191 }
192
193 if saw { prefix } else { None }
194}
195
196fn display_key_for_storage(
197 action_key: &str,
198 section_stack: &[String],
199 root_prefix: Option<&str>,
200) -> String {
201 if let Some(section) = section_stack.last() {
202 let needle = section.clone() + "_";
203 if let Some(pos) = action_key.find(&needle) {
204 let start = pos + needle.len();
205 if start < action_key.len() {
206 return action_key[start..].to_string();
207 }
208 }
209 if let Some(stripped) = action_key.strip_prefix(&needle) {
210 return stripped.to_string();
211 }
212 return action_key.to_string();
213 }
214
215 if let Some(prefix) = root_prefix {
216 let needle = prefix.to_string() + "_";
217 if let Some(stripped) = action_key.strip_prefix(&needle) {
218 return stripped.to_string();
219 }
220 }
221
222 action_key.to_string()
223}
224
225fn candidate_input_keys(
226 action_key: &str,
227 section_stack: &[String],
228 root_prefix: Option<&str>,
229) -> Vec<String> {
230 let mut keys = vec![display_key_for_storage(
231 action_key,
232 section_stack,
233 root_prefix,
234 )];
235 keys.push(action_key.to_string());
236
237 if let Some(section) = section_stack.last() {
238 let needle = section.clone() + "_";
239 if let Some(stripped) = action_key.strip_prefix(&needle) {
240 keys.push(stripped.to_string());
241 }
242 if let Some(pos) = action_key.find(&needle) {
243 let start = pos + needle.len();
244 if start < action_key.len() {
245 keys.push(action_key[start..].to_string());
246 }
247 }
248 }
249
250 keys.sort();
251 keys.dedup();
252 keys
253}
254
255fn special_action_section_key(action_key: &str) -> Option<(&'static str, &'static str)> {
256 match action_key {
257 "iso_hide_on_enter" => Some(("iso", "hide_on_enter")),
258 _ => None,
259 }
260}
261
262fn section_table<'a>(table: &'a toml::Table, path: &[String]) -> Option<&'a toml::Table> {
263 if path.is_empty() {
264 return Some(table);
265 }
266
267 let key = &path[0];
268 let value = table.get(key)?;
269 let sub = value.as_table()?;
270 section_table(sub, &path[1..])
271}
272
273pub fn nodeui_to_toml(nodeui: &TheNodeUI) -> String {
276 fn upsert(
277 entries: &mut Vec<(String, toml::Value, Option<String>)>,
278 key: String,
279 value: toml::Value,
280 comment: Option<String>,
281 ) {
282 if let Some((_, existing, existing_comment)) =
283 entries.iter_mut().find(|(k, _, _)| *k == key)
284 {
285 *existing = value;
286 *existing_comment = comment;
287 } else {
288 entries.push((key, value, comment));
289 }
290 }
291
292 fn selector_options_comment(values: &[String]) -> String {
293 let quoted = values
294 .iter()
295 .map(|v| format!("\"{}\"", v.replace('"', "\\\"")))
296 .collect::<Vec<_>>()
297 .join(", ");
298 format!("# {quoted}")
299 }
300
301 fn parse_string_array(value: &str) -> Vec<String> {
302 if let Ok(toml_value) = value.parse::<toml::Value>()
303 && let toml::Value::Array(items) = toml_value
304 {
305 let parsed: Vec<String> = items
306 .iter()
307 .filter_map(|item| item.as_str().map(|s| s.trim().to_string()))
308 .filter(|s| !s.is_empty())
309 .collect();
310 if !parsed.is_empty() {
311 return parsed;
312 }
313 }
314
315 value
316 .split(',')
317 .map(|item| item.trim())
318 .filter(|item| !item.is_empty())
319 .map(|item| item.trim_matches('"').to_string())
320 .collect()
321 }
322
323 fn section_entries_mut<'a>(
324 sections: &'a mut Vec<(String, Vec<(String, toml::Value, Option<String>)>)>,
325 name: &str,
326 ) -> &'a mut Vec<(String, toml::Value, Option<String>)> {
327 if let Some(index) = sections.iter().position(|(n, _)| n == name) {
328 return &mut sections[index].1;
329 }
330 sections.push((name.to_string(), Vec::new()));
331 let last = sections.len() - 1;
332 &mut sections[last].1
333 }
334
335 let mut root_action_entries: Vec<(String, toml::Value, Option<String>)> = vec![];
336 let mut sections: Vec<(String, Vec<(String, toml::Value, Option<String>)>)> = vec![];
337 let mut section_stack: Vec<String> = vec![];
338 let mut has_editable_values = false;
339 let root_prefix = root_table_prefix(nodeui);
340
341 for (_, item) in nodeui.list_items() {
342 match item {
343 TheNodeUIItem::OpenTree(name) => {
344 section_stack.push(normalize_toml_key(name));
345 }
346 TheNodeUIItem::CloseTree => {
347 section_stack.pop();
348 }
349 TheNodeUIItem::Text(id, _, _, value, _, _) => {
350 let action_key = action_param_key(id);
351 let (target_section, key) = if section_stack.is_empty() {
352 if let Some((section, special_key)) = special_action_section_key(&action_key) {
353 (Some(section.to_string()), special_key.to_string())
354 } else {
355 (
356 None,
357 display_key_for_storage(
358 &action_key,
359 §ion_stack,
360 root_prefix.as_deref(),
361 ),
362 )
363 }
364 } else {
365 (
366 Some(section_stack.join(".")),
367 display_key_for_storage(
368 &action_key,
369 §ion_stack,
370 root_prefix.as_deref(),
371 ),
372 )
373 };
374 let val = if action_key == "iso_hide_on_enter" {
375 toml::Value::Array(
376 parse_string_array(value)
377 .into_iter()
378 .map(toml::Value::String)
379 .collect(),
380 )
381 } else {
382 toml::Value::String(value.clone())
383 };
384 if let Some(section_name) = target_section {
385 let entries = section_entries_mut(&mut sections, §ion_name);
386 upsert(entries, key, val, None);
387 } else {
388 upsert(&mut root_action_entries, key, val, None);
389 }
390 has_editable_values = true;
391 }
392 TheNodeUIItem::Selector(id, _, _, values, index) => {
393 let action_key = action_param_key(id);
394 let key =
395 display_key_for_storage(&action_key, §ion_stack, root_prefix.as_deref());
396 let selected = if (*index as usize) < values.len() {
397 toml::Value::String(values[*index as usize].clone())
398 } else {
399 toml::Value::Integer(*index as i64)
400 };
401 let comment = Some(selector_options_comment(values));
402 if section_stack.is_empty() {
403 upsert(&mut root_action_entries, key, selected, comment);
404 } else {
405 let section_name = section_stack.join(".");
406 let entries = section_entries_mut(&mut sections, §ion_name);
407 upsert(entries, key, selected, comment);
408 }
409 has_editable_values = true;
410 }
411 TheNodeUIItem::FloatEditSlider(id, _, _, value, _, _)
412 | TheNodeUIItem::FloatSlider(id, _, _, value, _, _, _) => {
413 let action_key = action_param_key(id);
414 let key =
415 display_key_for_storage(&action_key, §ion_stack, root_prefix.as_deref());
416 let val = toml::Value::Float(round_f64_3(*value as f64));
417 if section_stack.is_empty() {
418 upsert(&mut root_action_entries, key, val, None);
419 } else {
420 let section_name = section_stack.join(".");
421 let entries = section_entries_mut(&mut sections, §ion_name);
422 upsert(entries, key, val, None);
423 }
424 has_editable_values = true;
425 }
426 TheNodeUIItem::IntEditSlider(id, _, _, value, _, _)
427 | TheNodeUIItem::PaletteSlider(id, _, _, value, _, _)
428 | TheNodeUIItem::IntSlider(id, _, _, value, _, _, _) => {
429 let action_key = action_param_key(id);
430 let key =
431 display_key_for_storage(&action_key, §ion_stack, root_prefix.as_deref());
432 let val = toml::Value::Integer(*value as i64);
433 if section_stack.is_empty() {
434 upsert(&mut root_action_entries, key, val, None);
435 } else {
436 let section_name = section_stack.join(".");
437 let entries = section_entries_mut(&mut sections, §ion_name);
438 upsert(entries, key, val, None);
439 }
440 has_editable_values = true;
441 }
442 TheNodeUIItem::ColorPicker(id, _, _, value, _) => {
443 let action_key = action_param_key(id);
444 let key =
445 display_key_for_storage(&action_key, §ion_stack, root_prefix.as_deref());
446 let val = toml::Value::String(value.to_hex());
447 if section_stack.is_empty() {
448 upsert(&mut root_action_entries, key, val, None);
449 } else {
450 let section_name = section_stack.join(".");
451 let entries = section_entries_mut(&mut sections, §ion_name);
452 upsert(entries, key, val, None);
453 }
454 has_editable_values = true;
455 }
456 TheNodeUIItem::Checkbox(id, _, _, value) => {
457 let action_key = action_param_key(id);
458 let key =
459 display_key_for_storage(&action_key, §ion_stack, root_prefix.as_deref());
460 let val = toml::Value::Boolean(*value);
461 if section_stack.is_empty() {
462 upsert(&mut root_action_entries, key, val, None);
463 } else {
464 let section_name = section_stack.join(".");
465 let entries = section_entries_mut(&mut sections, §ion_name);
466 upsert(entries, key, val, None);
467 }
468 has_editable_values = true;
469 }
470 TheNodeUIItem::Button(_, _, _, _)
471 | TheNodeUIItem::Markdown(_, _)
472 | TheNodeUIItem::Separator(_)
473 | TheNodeUIItem::Icons(_, _, _, _) => {}
474 }
475 }
476
477 if !has_editable_values {
478 return String::new();
479 }
480
481 let mut out = String::new();
482
483 if !root_action_entries.is_empty() {
484 out.push_str("[action]\n");
485 for (key, value, comment) in &root_action_entries {
486 if let Some(comment) = comment {
487 out.push_str(comment);
488 out.push('\n');
489 }
490 out.push_str(&format!("{key} = {value}\n"));
491 }
492 }
493
494 for (section, entries) in §ions {
495 if entries.is_empty() {
496 continue;
497 }
498 if !out.is_empty() {
499 out.push('\n');
500 }
501 out.push_str(&format!("[{section}]\n"));
502 for (key, value, comment) in entries {
503 if let Some(comment) = comment {
504 out.push_str(comment);
505 out.push('\n');
506 }
507 out.push_str(&format!("{key} = {value}\n"));
508 }
509 }
510
511 out
512}
513
514pub fn apply_toml_to_nodeui(nodeui: &mut TheNodeUI, source: &str) -> Result<(), String> {
517 let root: toml::Table = toml::from_str(source).map_err(|e| e.to_string())?;
518 let action_root = root
519 .get("action")
520 .and_then(|v| v.as_table())
521 .unwrap_or(&root);
522 let mut section_stack: Vec<String> = vec![];
523 let items: Vec<TheNodeUIItem> = nodeui.list_items().map(|(_, item)| item.clone()).collect();
524 let root_prefix = root_table_prefix(nodeui);
525
526 for item in items {
527 match item {
528 TheNodeUIItem::OpenTree(name) => {
529 section_stack.push(normalize_toml_key(&name));
530 }
531 TheNodeUIItem::CloseTree => {
532 section_stack.pop();
533 }
534 TheNodeUIItem::Text(id, _, _, _, _, _) => {
535 let action_key = action_param_key(&id);
536 let table = if section_stack.is_empty() {
537 if let Some((section, _)) = special_action_section_key(&action_key) {
538 section_table(&root, &[section.to_string()]).or(Some(action_root))
539 } else {
540 Some(action_root)
541 }
542 } else {
543 section_table(&root, §ion_stack)
544 .or_else(|| section_table(action_root, §ion_stack))
545 };
546 if let Some(table) = table {
547 let mut keys =
548 candidate_input_keys(&action_key, §ion_stack, root_prefix.as_deref());
549 if section_stack.is_empty()
550 && let Some((_, special_key)) = special_action_section_key(&action_key)
551 {
552 keys.push(special_key.to_string());
553 keys.sort();
554 keys.dedup();
555 }
556 for key in keys {
557 if let Some(value) = table.get(&key) {
558 match value {
559 toml::Value::String(v) => {
560 nodeui.set_text_value(&id, v.clone());
561 break;
562 }
563 toml::Value::Integer(v) => {
564 nodeui.set_text_value(&id, v.to_string());
565 break;
566 }
567 toml::Value::Float(v) => {
568 nodeui.set_text_value(&id, v.to_string());
569 break;
570 }
571 toml::Value::Array(items) if action_key == "iso_hide_on_enter" => {
572 let joined = items
573 .iter()
574 .filter_map(|item| item.as_str())
575 .collect::<Vec<_>>()
576 .join(", ");
577 nodeui.set_text_value(&id, joined);
578 break;
579 }
580 _ => {}
581 }
582 }
583 }
584 }
585 }
586 TheNodeUIItem::Selector(id, _, _, values, _) => {
587 let action_key = action_param_key(&id);
588 let table = if section_stack.is_empty() {
589 Some(action_root)
590 } else {
591 section_table(&root, §ion_stack)
592 .or_else(|| section_table(action_root, §ion_stack))
593 };
594 if let Some(table) = table {
595 for key in
596 candidate_input_keys(&action_key, §ion_stack, root_prefix.as_deref())
597 {
598 if let Some(value) = table.get(&key) {
599 match value {
600 toml::Value::Integer(v) => nodeui.set_i32_value(&id, *v as i32),
601 toml::Value::String(name) => {
602 if let Some(index) = values.iter().position(|v| v == name) {
603 nodeui.set_i32_value(&id, index as i32);
604 }
605 }
606 _ => {}
607 }
608 break;
609 }
610 }
611 }
612 }
613 TheNodeUIItem::FloatEditSlider(id, _, _, _, _, _)
614 | TheNodeUIItem::FloatSlider(id, _, _, _, _, _, _) => {
615 let action_key = action_param_key(&id);
616 let table = if section_stack.is_empty() {
617 Some(action_root)
618 } else {
619 section_table(&root, §ion_stack)
620 .or_else(|| section_table(action_root, §ion_stack))
621 };
622 if let Some(table) = table {
623 for key in
624 candidate_input_keys(&action_key, §ion_stack, root_prefix.as_deref())
625 {
626 if let Some(value) = table.get(&key) {
627 match value {
628 toml::Value::Float(v) => nodeui.set_f32_value(&id, *v as f32),
629 toml::Value::Integer(v) => nodeui.set_f32_value(&id, *v as f32),
630 _ => {}
631 }
632 break;
633 }
634 }
635 }
636 }
637 TheNodeUIItem::IntEditSlider(id, _, _, _, _, _)
638 | TheNodeUIItem::PaletteSlider(id, _, _, _, _, _)
639 | TheNodeUIItem::IntSlider(id, _, _, _, _, _, _) => {
640 let action_key = action_param_key(&id);
641 let table = if section_stack.is_empty() {
642 Some(action_root)
643 } else {
644 section_table(&root, §ion_stack)
645 .or_else(|| section_table(action_root, §ion_stack))
646 };
647 if let Some(table) = table {
648 for key in
649 candidate_input_keys(&action_key, §ion_stack, root_prefix.as_deref())
650 {
651 if let Some(value) = table.get(&key) {
652 if let toml::Value::Integer(v) = value {
653 nodeui.set_i32_value(&id, *v as i32);
654 }
655 break;
656 }
657 }
658 }
659 }
660 TheNodeUIItem::ColorPicker(id, _, _, _, _) => {
661 let action_key = action_param_key(&id);
662 let table = if section_stack.is_empty() {
663 Some(action_root)
664 } else {
665 section_table(&root, §ion_stack)
666 .or_else(|| section_table(action_root, §ion_stack))
667 };
668 if let Some(table) = table {
669 for key in
670 candidate_input_keys(&action_key, §ion_stack, root_prefix.as_deref())
671 {
672 if let Some(toml::Value::String(v)) = table.get(&key) {
673 if let Some(TheNodeUIItem::ColorPicker(_, _, _, color, _)) =
674 nodeui.get_item_mut(&id)
675 {
676 *color = TheColor::from_hex(v);
677 }
678 break;
679 }
680 }
681 }
682 }
683 TheNodeUIItem::Checkbox(id, _, _, _) => {
684 let action_key = action_param_key(&id);
685 let table = if section_stack.is_empty() {
686 Some(action_root)
687 } else {
688 section_table(&root, §ion_stack)
689 .or_else(|| section_table(action_root, §ion_stack))
690 };
691 if let Some(table) = table {
692 for key in
693 candidate_input_keys(&action_key, §ion_stack, root_prefix.as_deref())
694 {
695 if let Some(toml::Value::Boolean(v)) = table.get(&key) {
696 nodeui.set_bool_value(&id, *v);
697 break;
698 }
699 }
700 }
701 }
702 TheNodeUIItem::Button(_, _, _, _)
703 | TheNodeUIItem::Markdown(_, _)
704 | TheNodeUIItem::Separator(_)
705 | TheNodeUIItem::Icons(_, _, _, _) => {}
706 }
707 }
708
709 Ok(())
710}
711
712pub fn nodeui_to_value_pairs(nodeui: &TheNodeUI) -> Vec<(String, TheValue)> {
715 let mut out: Vec<(String, TheValue)> = Vec::new();
716 for (_, item) in nodeui.list_items() {
717 match item {
718 TheNodeUIItem::Text(id, _, _, value, _, _) => {
719 out.push((id.clone(), TheValue::Text(value.clone())));
720 }
721 TheNodeUIItem::Selector(id, _, _, _, value) => {
722 out.push((id.clone(), TheValue::Int(*value)));
723 }
724 TheNodeUIItem::FloatEditSlider(id, _, _, value, _, _)
725 | TheNodeUIItem::FloatSlider(id, _, _, value, _, _, _) => {
726 out.push((id.clone(), TheValue::Float(*value)));
727 }
728 TheNodeUIItem::IntEditSlider(id, _, _, value, _, _)
729 | TheNodeUIItem::PaletteSlider(id, _, _, value, _, _)
730 | TheNodeUIItem::IntSlider(id, _, _, value, _, _, _) => {
731 out.push((id.clone(), TheValue::Int(*value)));
732 }
733 TheNodeUIItem::ColorPicker(id, _, _, value, _) => {
734 out.push((id.clone(), TheValue::ColorObject(value.clone())));
735 }
736 TheNodeUIItem::Checkbox(id, _, _, value) => {
737 out.push((id.clone(), TheValue::Bool(*value)));
738 }
739 TheNodeUIItem::Button(_, _, _, _)
740 | TheNodeUIItem::Markdown(_, _)
741 | TheNodeUIItem::Separator(_)
742 | TheNodeUIItem::Icons(_, _, _, _)
743 | TheNodeUIItem::OpenTree(_)
744 | TheNodeUIItem::CloseTree => {}
745 }
746 }
747 out
748}