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}