Skip to main content

pfx_editor/
level.rs

1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3
4use egui::Pos2;
5use glam::DVec3;
6use pfx_editor_viewport::pick::matrix;
7use pfx_editor_viewport::{Eye, Lens};
8use pfx_load::scene::tiles::{self, Tiles};
9use pfx_load::scene::{EditError, Id, Kind, PatchGroup, Scene, Target, Value};
10
11use crate::editor::Editor;
12use crate::outline::Item;
13use crate::snap::{self, Snapping, Surfaces, VERTEX_POINTS};
14
15pub const OFFSET: [f32; 3] = [1.0, 0.0, 0.0];
16const TRIES: u32 = 16;
17const PLACES: f64 = 1e4;
18
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
20pub enum Tool {
21    #[default]
22    Paint,
23    Erase,
24    Pick,
25}
26
27impl Tool {
28    pub fn word(self) -> &'static str {
29        match self {
30            Tool::Paint => "paint",
31            Tool::Erase => "erase",
32            Tool::Pick => "pick",
33        }
34    }
35}
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub enum Shape {
39    Free,
40    Line,
41    Rectangle,
42}
43
44#[derive(Clone, Debug, PartialEq, Eq)]
45pub struct Stroke {
46    pub shape: Shape,
47    pub from: [i32; 2],
48    pub to: [i32; 2],
49    pub visited: Vec<[i32; 2]>,
50}
51
52impl Stroke {
53    pub fn new(shape: Shape, at: [i32; 2]) -> Stroke {
54        Stroke {
55            shape,
56            from: at,
57            to: at,
58            visited: vec![at],
59        }
60    }
61
62    pub fn reach(&mut self, at: [i32; 2]) {
63        if self.shape == Shape::Free && at != self.to {
64            for cell in tiles::line(self.to, at).into_iter().skip(1) {
65                if !self.visited.contains(&cell) {
66                    self.visited.push(cell);
67                }
68            }
69        }
70        self.to = at;
71    }
72
73    pub fn cells(&self) -> Vec<[i32; 2]> {
74        match self.shape {
75            Shape::Free => self.visited.clone(),
76            Shape::Line => tiles::line(self.from, self.to),
77            Shape::Rectangle => tiles::rectangle(self.from, self.to),
78        }
79    }
80}
81
82#[derive(Clone, Debug, Default, PartialEq)]
83pub struct Brush {
84    pub on: bool,
85    pub tool: Tool,
86    pub layer: Option<String>,
87    pub prefab: Option<String>,
88    pub stroke: Option<Stroke>,
89    pub grab: Option<(Eye, Pos2, bool)>,
90}
91
92#[derive(Clone, Copy, Debug, PartialEq)]
93pub struct Aim {
94    pub point: DVec3,
95    pub normal: DVec3,
96    pub at: [f32; 3],
97    pub rotate: [f32; 3],
98}
99
100#[derive(Default)]
101pub struct Level {
102    pub snapping: Snapping,
103    pub stamp: Option<PathBuf>,
104    pub picked: Vec<String>,
105    pub offset: [f32; 3],
106    pub brush: Brush,
107    pub shift: bool,
108    pub aim: Option<Aim>,
109    surfaces: Option<(BTreeMap<PathBuf, [u8; 32]>, usize, Surfaces)>,
110    extents: BTreeMap<PathBuf, Result<[[f32; 3]; 2], String>>,
111}
112
113impl Level {
114    pub fn new() -> Level {
115        Level {
116            offset: OFFSET,
117            ..Level::default()
118        }
119    }
120
121    pub fn shown(&self) -> bool {
122        self.brush.on
123            || self.stamp.is_some()
124            || self.picked.len() > 1
125            || self.snapping != Snapping::default()
126    }
127}
128
129pub fn placeable(path: &Path) -> bool {
130    let name = path.to_string_lossy().to_lowercase();
131    name.ends_with(".prefab.toml") || name.ends_with(".gltf") || name.ends_with(".glb")
132}
133
134pub fn stem(path: &Path) -> String {
135    let name = path
136        .file_name()
137        .map(|name| name.to_string_lossy().into_owned())
138        .unwrap_or_default();
139    let lower = name.to_lowercase();
140    for suffix in [".prefab.toml", ".gltf", ".glb"] {
141        if lower.ends_with(suffix) {
142            return name[..name.len() - suffix.len()].to_string();
143        }
144    }
145    name
146}
147
148pub fn unique(taken: impl Fn(&str) -> bool, stem: &str) -> String {
149    let stem = if stem.trim().is_empty() {
150        "object"
151    } else {
152        stem.trim()
153    };
154    if !taken(stem) {
155        return stem.to_string();
156    }
157    (2..)
158        .map(|count| format!("{stem} {count}"))
159        .find(|name| !taken(name))
160        .unwrap_or_default()
161}
162
163pub fn base(name: &str) -> &str {
164    match name.rsplit_once(' ') {
165        Some((base, count)) if !base.is_empty() && count.parse::<u32>().is_ok() => base,
166        _ => name,
167    }
168}
169
170fn round(value: f64) -> f32 {
171    let rounded = (value * PLACES).round() / PLACES;
172    if rounded == 0.0 { 0.0 } else { rounded as f32 }
173}
174
175fn rounded(point: DVec3) -> [f32; 3] {
176    point.to_array().map(round)
177}
178
179fn taken(error: &EditError) -> bool {
180    error.message.contains("is taken")
181}
182
183fn world(scene: &Scene, name: &str) -> Option<DVec3> {
184    let model = scene.object(name)?.model;
185    Some(DVec3::new(
186        f64::from(model[3][0]),
187        f64::from(model[3][1]),
188        f64::from(model[3][2]),
189    ))
190}
191
192fn local(scene: &Scene, name: &str, point: DVec3) -> Option<DVec3> {
193    let object = scene.object(name)?;
194    Some(
195        match object.parent.as_deref().and_then(|p| scene.object(p)) {
196            Some(parent) => matrix(parent.model).inverse().transform_point3(point),
197            None => point,
198        },
199    )
200}
201
202impl Editor {
203    pub fn relative(&self, file: &Path) -> String {
204        let root = self.project_root();
205        let root = std::path::absolute(root).unwrap_or_else(|_| root.to_path_buf());
206        let file = std::path::absolute(file).unwrap_or_else(|_| file.to_path_buf());
207        file.strip_prefix(&root)
208            .unwrap_or(&file)
209            .to_string_lossy()
210            .replace('\\', "/")
211    }
212
213    fn derived(&self, kind: &str, name: &str, salt: u32) -> Id {
214        let scene = self.relative(self.path());
215        Id::derive(format!("{scene}\n{kind}\n{name}\n{salt}").as_bytes())
216    }
217
218    pub fn layers(&self) -> Vec<Tiles> {
219        self.edit
220            .resolved()
221            .tiles
222            .iter()
223            .map(|entry| entry.value.clone())
224            .collect()
225    }
226
227    pub fn is_cell(&self, object: &str) -> bool {
228        let Some((layer, _)) = tiles::cell_of(object) else {
229            return false;
230        };
231        self.edit
232            .resolved()
233            .tiles
234            .iter()
235            .any(|entry| entry.key == layer)
236    }
237
238    pub fn surfaces(&mut self) -> Option<&Surfaces> {
239        let scene = self.viewport.scene()?;
240        let fresh = self
241            .level
242            .surfaces
243            .as_ref()
244            .is_none_or(|(files, count, _)| files != &scene.files || *count != scene.objects.len());
245        if fresh {
246            self.level.surfaces = Some((
247                scene.files.clone(),
248                scene.objects.len(),
249                Surfaces::of(scene),
250            ));
251        }
252        self.level
253            .surfaces
254            .as_ref()
255            .map(|(_, _, surfaces)| surfaces)
256    }
257
258    pub fn lens(&self) -> Option<Lens> {
259        Some(Lens::new(&self.viewport.eye()?, self.viewport.image()?))
260    }
261
262    pub fn extent(&mut self, asset: &Path) -> Result<[[f32; 3]; 2], String> {
263        if let Some(found) = self.level.extents.get(asset) {
264            return found.clone();
265        }
266        let relative = self.relative(asset);
267        let found = tiles::extent(self.project_root(), &relative).map_err(|e| e.to_string());
268        self.level
269            .extents
270            .insert(asset.to_path_buf(), found.clone());
271        found
272    }
273
274    pub fn aim(&mut self, asset: &Path, at: Pos2) -> Result<Aim, String> {
275        let lens = self.lens().ok_or("the viewport has not been laid out")?;
276        let ray = lens.ray(at);
277        let snapping = self.level.snapping;
278        let bounds = self.extent(asset)?;
279        let surfaces = self.surfaces();
280        let hit = surfaces
281            .as_ref()
282            .filter(|_| snapping.surface)
283            .and_then(|surfaces| surfaces.cast(&ray));
284        let (mut point, normal) = match hit {
285            Some(hit) => (hit.point, hit.normal),
286            None => (
287                snap::ground(&ray).ok_or("nothing under the pointer to place on")?,
288                DVec3::Y,
289            ),
290        };
291        if snapping.vertex
292            && let Some(vertex) = surfaces
293                .as_ref()
294                .and_then(|surfaces| surfaces.vertex(&lens, at, VERTEX_POINTS, &[]))
295        {
296            point = vertex;
297        }
298        if snapping.grid {
299            point = snap::grid(point, snapping.step, snap::across(normal));
300        }
301        let rotate = if snapping.align {
302            snap::upright(normal)
303        } else {
304            DVec3::ZERO
305        };
306        let lifted = point + snap::lift(bounds, rotate, normal);
307        Ok(Aim {
308            point,
309            normal,
310            at: rounded(lifted),
311            rotate: rounded(rotate),
312        })
313    }
314
315    pub fn place(&mut self, asset: &Path, at: Pos2) -> Option<String> {
316        if !self.editable() {
317            return None;
318        }
319        let aim = match self.aim(asset, at) {
320            Ok(aim) => aim,
321            Err(error) => {
322                self.log.warn(format!("place {}: {error}", stem(asset)));
323                return None;
324            }
325        };
326        match self.place_at(asset, aim.at, aim.rotate) {
327            Ok(name) => Some(name),
328            Err(error) => {
329                self.log.warn(error);
330                None
331            }
332        }
333    }
334
335    pub fn place_at(
336        &mut self,
337        asset: &Path,
338        at: [f32; 3],
339        rotate: [f32; 3],
340    ) -> Result<String, String> {
341        if !placeable(asset) {
342            return Err(format!(
343                "{}: only a *.prefab.toml or a glTF can be placed",
344                self.relative(asset)
345            ));
346        }
347        self.end_group();
348        let relative = self.relative(asset);
349        let prefab = relative.to_lowercase().ends_with(".prefab.toml");
350        let scene = self.scene().clone();
351        let name = unique(|name| scene.object(name).is_some(), &stem(asset));
352        let file = self.project_root().join(&relative);
353        let mesh = scene
354            .meshes
355            .iter()
356            .find(|(key, mesh)| {
357                mesh.file == file && mesh.node.is_none() && !key.contains(['/', ':'])
358            })
359            .map(|(key, _)| key.clone());
360        let mesh_name = mesh
361            .clone()
362            .unwrap_or_else(|| unique(|key| scene.meshes.contains_key(key), &stem(asset)));
363        let mut fields: Vec<(&str, Value)> = Vec::new();
364        if prefab {
365            fields.push(("prefab", Value::from(relative.as_str())));
366        } else {
367            fields.push(("mesh", Value::from(mesh_name.as_str())));
368        }
369        fields.push(("at", Value::from(at)));
370        if rotate != [0.0; 3] {
371            fields.push(("rotate", Value::from(rotate)));
372        }
373        let group = self.salted(format!("place {name}: no free id"), |editor, salt| {
374            let id = editor.derived("object", &name, salt);
375            let mesh_id = editor.derived("mesh", &mesh_name, salt);
376            editor.edit.dry_run(|edit| {
377                if !prefab && mesh.is_none() {
378                    edit.add_mesh(mesh_id, &mesh_name, &relative, None)?;
379                }
380                edit.add(Kind::Object, id, &name, &fields, None)
381            })
382        })?;
383        self.land(labelled(group, format!("place {name}")));
384        if self.scene().object(&name).is_none() {
385            return Err(format!("place {name}: the scene did not take it"));
386        }
387        self.level.stamp = Some(asset.to_path_buf());
388        self.select(Some(Item::Object(name.clone())));
389        Ok(name)
390    }
391
392    pub fn picked(&self) -> Vec<String> {
393        if self.level.picked.is_empty() {
394            return self
395                .selected_object()
396                .map(str::to_string)
397                .into_iter()
398                .collect();
399        }
400        self.level.picked.clone()
401    }
402
403    pub fn pick_more(&mut self, name: &str) {
404        let mut picked = self.picked();
405        match picked.iter().position(|found| found == name) {
406            Some(at) if picked.len() > 1 => {
407                picked.remove(at);
408            }
409            Some(_) => {}
410            None => picked.push(name.to_string()),
411        }
412        let primary = picked.last().cloned();
413        self.select(primary.map(Item::Object));
414        self.level.picked = picked;
415    }
416
417    fn movable(&mut self, verb: &str) -> Vec<String> {
418        let picked = self.picked();
419        let mut kept = Vec::new();
420        for name in picked {
421            if self.is_cell(&name) {
422                self.log.warn(format!(
423                    "{verb}: {name} is a tile; paint its layer with the brush instead"
424                ));
425            } else if self.scene().object(&name).is_some() {
426                kept.push(name);
427            }
428        }
429        kept
430    }
431
432    pub fn duplicate(&mut self) -> Vec<String> {
433        if !self.editable() {
434            return Vec::new();
435        }
436        self.end_group();
437        let names = self.movable("duplicate");
438        if names.is_empty() {
439            return Vec::new();
440        }
441        let scene = self.scene().clone();
442        let offset = self.level.offset;
443        let mut made: Vec<String> = Vec::new();
444        let mut plan = Vec::new();
445        for name in &names {
446            if name.contains('/') {
447                self.log.warn(format!(
448                    "duplicate: {name} is placed by a prefab; duplicate its placement"
449                ));
450                continue;
451            }
452            let Some(object) = scene.object(name) else {
453                continue;
454            };
455            let new = unique(
456                |candidate| {
457                    scene.object(candidate).is_some() || made.iter().any(|m| m == candidate)
458                },
459                base(name),
460            );
461            let at: [f32; 3] = std::array::from_fn(|k| object.at[k] + offset[k]);
462            made.push(new.clone());
463            plan.push((name.clone(), new, at));
464        }
465        if plan.is_empty() {
466            return Vec::new();
467        }
468        let label = match plan.len() {
469            1 => format!("duplicate {}", plan[0].0),
470            count => format!("duplicate {count} objects"),
471        };
472        let outcome = self.salted(format!("{label}: no free id"), |editor, salt| {
473            let ids: Vec<Id> = plan
474                .iter()
475                .map(|(_, new, _)| editor.derived("object", new, salt))
476                .collect();
477            editor.edit.dry_run(|edit| {
478                for ((name, new, at), id) in plan.iter().zip(&ids) {
479                    edit.duplicate_object(name, new, *id)?;
480                    edit.set_at(new, *at)?;
481                }
482                Ok(())
483            })
484        });
485        match outcome {
486            Ok(group) => {
487                self.land(labelled(group, label));
488                let primary = made.last().cloned();
489                self.select(primary.map(Item::Object));
490                self.level.picked = made.clone();
491                made
492            }
493            Err(error) => {
494                self.log.warn(error);
495                Vec::new()
496            }
497        }
498    }
499
500    fn moves(&mut self, label: String, moves: Vec<(String, DVec3)>) -> bool {
501        let scene = self.scene().clone();
502        let mut writes = Vec::new();
503        for (name, point) in moves {
504            let Some(local) = local(&scene, &name, point) else {
505                continue;
506            };
507            writes.push((name, rounded(local)));
508        }
509        let found = self.edit.dry_run(|edit| {
510            for (name, at) in &writes {
511                edit.set_at(name, *at)?;
512            }
513            Ok(())
514        });
515        match found {
516            Ok(group) if group.is_empty() => {
517                self.log.info(format!("{label}: already in place"));
518                false
519            }
520            Ok(group) => {
521                self.land(labelled(group, label));
522                true
523            }
524            Err(error) => {
525                self.log.edit_error(&error);
526                false
527            }
528        }
529    }
530
531    pub fn align(&mut self, axis: usize, to_last: bool) -> bool {
532        if !self.editable() || axis > 2 {
533            return false;
534        }
535        self.end_group();
536        let names = self.movable("align");
537        if names.len() < 2 {
538            self.log
539                .info("align: select two objects or more (shift-click)");
540            return false;
541        }
542        let scene = self.scene().clone();
543        let anchor = if to_last {
544            &names[names.len() - 1]
545        } else {
546            &names[0]
547        };
548        let Some(target) = world(&scene, anchor) else {
549            return false;
550        };
551        let moves = names
552            .iter()
553            .filter(|name| *name != anchor)
554            .filter_map(|name| {
555                let mut point = world(&scene, name)?;
556                point[axis] = target[axis];
557                Some((name.clone(), point))
558            })
559            .collect();
560        let label = format!(
561            "align {} objects in {} to {anchor}",
562            names.len(),
563            ["x", "y", "z"][axis]
564        );
565        self.moves(label, moves)
566    }
567
568    pub fn distribute(&mut self, axis: usize) -> bool {
569        if !self.editable() || axis > 2 {
570            return false;
571        }
572        self.end_group();
573        let names = self.movable("distribute");
574        if names.len() < 3 {
575            self.log
576                .info("distribute: select three objects or more (shift-click)");
577            return false;
578        }
579        let scene = self.scene().clone();
580        let mut placed: Vec<(String, DVec3)> = names
581            .iter()
582            .filter_map(|name| Some((name.clone(), world(&scene, name)?)))
583            .collect();
584        placed.sort_by(|a, b| a.1[axis].total_cmp(&b.1[axis]).then_with(|| a.0.cmp(&b.0)));
585        let (first, last) = (placed[0].1[axis], placed[placed.len() - 1].1[axis]);
586        let gap = (last - first) / (placed.len() - 1) as f64;
587        let moves = placed
588            .iter()
589            .enumerate()
590            .skip(1)
591            .take(placed.len() - 2)
592            .map(|(place, (name, point))| {
593                let mut point = *point;
594                point[axis] = first + gap * place as f64;
595                (name.clone(), point)
596            })
597            .collect();
598        let label = format!(
599            "distribute {} objects along {}",
600            names.len(),
601            ["x", "y", "z"][axis]
602        );
603        self.moves(label, moves)
604    }
605
606    pub fn brush_layer(&self) -> Option<Tiles> {
607        let layers = self.layers();
608        let wanted = self.level.brush.layer.as_deref();
609        layers
610            .iter()
611            .find(|layer| Some(layer.name.as_str()) == wanted)
612            .or(layers.first())
613            .cloned()
614    }
615
616    pub fn brush_prefab(&mut self) -> Option<String> {
617        if let Some(prefab) = &self.level.brush.prefab {
618            return Some(prefab.clone());
619        }
620        if let Some(stamp) = self.level.stamp.clone()
621            && stamp.to_string_lossy().ends_with(".prefab.toml")
622        {
623            return Some(self.relative(&stamp));
624        }
625        self.brush_layer()?.palette.values().next().cloned()
626    }
627
628    pub fn cell_under(&mut self, at: Pos2) -> Option<[i32; 2]> {
629        let layer = self.brush_layer()?;
630        let lens = self.lens()?;
631        let ray = lens.ray(at);
632        let [across, up] = tiles::axes(&layer);
633        let normal =
634            DVec3::from_array(across.map(f64::from)).cross(DVec3::from_array(up.map(f64::from)));
635        let point = snap::plane(&ray, DVec3::from_array(layer.origin.map(f64::from)), normal)?;
636        Some(tiles::cell_at(&layer, point.to_array()))
637    }
638
639    pub fn paint(&mut self, cells: &[[i32; 2]], erase: bool) -> Result<usize, String> {
640        if !self.editable() {
641            return Err("paint: the scene is playing".to_string());
642        }
643        self.end_group();
644        let mut layer = self
645            .brush_layer()
646            .ok_or("paint: this scene has no tile layer ([[tiles]])")?;
647        let mut joined = None;
648        let key = if erase {
649            None
650        } else {
651            let prefab = self
652                .brush_prefab()
653                .ok_or("paint: choose a prefab for the brush first")?;
654            match tiles::key(&layer, &prefab) {
655                Some(key) => Some(key),
656                None => {
657                    let key = tiles::free_key(&layer).ok_or_else(|| {
658                        format!("paint: layer {}: its palette is full", layer.name)
659                    })?;
660                    layer.palette.insert(key.clone(), prefab.clone());
661                    joined = Some((key.clone(), prefab));
662                    Some(key)
663                }
664            }
665        };
666        let changes: Vec<([i32; 2], Option<&str>)> = cells
667            .iter()
668            .filter(|cell| tiles::get(&layer, **cell) != key)
669            .map(|cell| (*cell, key.as_deref()))
670            .collect();
671        if changes.is_empty() {
672            return Ok(0);
673        }
674        let painted = tiles::painted(&layer, &changes)?;
675        let count = changes.len();
676        let verb = if erase { "erase" } else { "paint" };
677        let label = format!(
678            "{verb} {count} {} in {}",
679            if count == 1 { "tile" } else { "tiles" },
680            layer.name
681        );
682        let target = Target::Tiles(layer.name.clone());
683        let old = layer.rows.clone().unwrap_or_default();
684        let found = self.edit.dry_run(|edit| {
685            if let Some((key, prefab)) = &joined {
686                edit.set(&target, &["palette", key], prefab.as_str())?;
687            }
688            match &painted {
689                tiles::Painted::Rows { rows, origin } => {
690                    if *origin != layer.origin {
691                        edit.set(&target, &["origin"], *origin)?;
692                    }
693                    if rows.len() == old.len() {
694                        for (place, row) in rows.iter().enumerate() {
695                            if row != &old[place] {
696                                let place = place.to_string();
697                                edit.set(&target, &["rows", &place], row.as_str())?;
698                            }
699                        }
700                    } else {
701                        let rows = rows.iter().map(|row| Value::from(row.as_str())).collect();
702                        edit.set(&target, &["rows"], Value::Array(rows))?;
703                    }
704                }
705                tiles::Painted::Cells(cells) => {
706                    let cells = cells
707                        .iter()
708                        .map(|cell| {
709                            Value::Table(vec![
710                                (
711                                    "at".to_string(),
712                                    Value::Array(vec![
713                                        Value::Int(i64::from(cell.at[0])),
714                                        Value::Int(i64::from(cell.at[1])),
715                                    ]),
716                                ),
717                                ("tile".to_string(), Value::from(cell.tile.as_str())),
718                            ])
719                        })
720                        .collect();
721                    edit.set(&target, &["cells"], Value::Array(cells))?;
722                }
723            }
724            Ok(())
725        });
726        match found {
727            Ok(group) if group.is_empty() => Ok(0),
728            Ok(group) => {
729                self.land(labelled(group, label));
730                Ok(count)
731            }
732            Err(error) => {
733                self.log.edit_error(&error);
734                Err(error.to_string())
735            }
736        }
737    }
738
739    pub fn pick_cell(&mut self, cell: [i32; 2]) -> Option<String> {
740        let layer = self.brush_layer()?;
741        let key = tiles::get(&layer, cell)?;
742        let prefab = layer.palette.get(&key)?.clone();
743        self.level.brush.prefab = Some(prefab.clone());
744        self.level.brush.tool = Tool::Paint;
745        self.log.info(format!("brush: {prefab}"));
746        Some(prefab)
747    }
748
749    pub fn finish_stroke(&mut self) {
750        let Some(stroke) = self.level.brush.stroke.take() else {
751            return;
752        };
753        let cells = stroke.cells();
754        let outcome = match self.level.brush.tool {
755            Tool::Pick => {
756                self.pick_cell(stroke.to);
757                return;
758            }
759            Tool::Paint => self.paint(&cells, false),
760            Tool::Erase => self.paint(&cells, true),
761        };
762        if let Err(error) = outcome {
763            self.log.warn(error);
764        }
765    }
766
767    pub fn toggle_brush(&mut self) {
768        let brush = &mut self.level.brush;
769        brush.on = !brush.on;
770        brush.stroke = None;
771        brush.grab = None;
772        if brush.on && self.edit.resolved().tiles.is_empty() {
773            self.log
774                .warn("brush: this scene has no tile layer; add a [[tiles]] table to it");
775        }
776    }
777}
778
779impl Editor {
780    fn salted(
781        &mut self,
782        none: String,
783        mut attempt: impl FnMut(&mut Self, u32) -> Result<PatchGroup, EditError>,
784    ) -> Result<PatchGroup, String> {
785        for salt in 0..TRIES {
786            match attempt(self, salt) {
787                Err(error) if taken(&error) => continue,
788                other => return other.map_err(|error| error.to_string()),
789            }
790        }
791        Err(none)
792    }
793}
794
795pub fn labelled(mut group: PatchGroup, label: String) -> PatchGroup {
796    for patch in &mut group.patches {
797        patch.label = label.clone();
798    }
799    group.label = label;
800    group
801}
802
803#[cfg(test)]
804mod tests {
805    use super::*;
806
807    #[test]
808    fn names_stems_and_strokes() {
809        assert_eq!(stem(Path::new("a/block.prefab.toml")), "block");
810        assert_eq!(stem(Path::new("Rock.GLB")), "Rock");
811        assert!(placeable(Path::new("x/rock.gltf")));
812        assert!(!placeable(Path::new("x/room.scene.toml")));
813        let taken = ["block", "block 2"];
814        assert_eq!(unique(|name| taken.contains(&name), "block"), "block 3");
815        assert_eq!(unique(|_| false, " "), "object");
816        assert_eq!(base("block 12"), "block");
817        assert_eq!(base("block two"), "block two");
818        assert_eq!(base(" 2"), " 2");
819        let mut free = Stroke::new(Shape::Free, [0, 0]);
820        free.reach([3, 0]);
821        free.reach([3, 0]);
822        free.reach([2, 0]);
823        assert_eq!(free.cells(), [[0, 0], [1, 0], [2, 0], [3, 0]]);
824        let mut line = Stroke::new(Shape::Line, [0, 0]);
825        line.reach([2, 2]);
826        assert_eq!(line.cells(), [[0, 0], [1, 1], [2, 2]]);
827        let mut rectangle = Stroke::new(Shape::Rectangle, [0, 0]);
828        rectangle.reach([1, 1]);
829        assert_eq!(rectangle.cells().len(), 4);
830        assert_eq!(round(0.123456), 0.1235);
831        assert_eq!(round(-0.00001), 0.0);
832        assert!(!Level::new().shown());
833    }
834}