Skip to main content

fyrox_impl/scene/tilemap/
brush.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! Tile map brush is a set of tiles arranged in arbitrary shape, that can be used to draw on a tile
22//! map. A brush resembles a tile set in that they are both collections of tiles, but they are distinct
23//! because a brush can be freely organized to suit whatever is most convenient while editing a tile map.
24//! The tiles of a brush can be moved at any time without consequence, and so can the pages.
25//! A brush is like a painter's toolbox, where the sole purpose is to serve the painter's convenience.
26//!
27//! In contrast, a tile set is a directory for finding tile data according to a specific position on
28//! a specific page. Tiles can be moved in a tile set, but doing so will affect the lookup of tile data.
29
30use fyrox_core::{futures::executor::block_on, log::Log};
31
32use crate::{
33    asset::{
34        io::ResourceIo,
35        loader::{BoxedLoaderFuture, LoaderPayload, ResourceLoader},
36        manager::ResourceManager,
37        state::LoadError,
38        untyped::UntypedResource,
39        Resource, ResourceData,
40    },
41    core::{
42        algebra::{Matrix4, Vector2, Vector3},
43        color::Color,
44        io::FileError,
45        reflect::prelude::*,
46        type_traits::prelude::*,
47        visitor::prelude::*,
48    },
49    scene::debug::SceneDrawingContext,
50};
51use std::{
52    error::Error,
53    fmt::{Display, Formatter},
54    path::{Path, PathBuf},
55    sync::Arc,
56};
57
58use super::*;
59
60/// An error that may occur during tile map brush resource loading.
61#[derive(Debug)]
62pub enum TileMapBrushResourceError {
63    /// An i/o error has occurred.
64    Io(FileError),
65
66    /// An error that may occur due to version incompatibilities.
67    Visit(VisitError),
68}
69
70impl Display for TileMapBrushResourceError {
71    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
72        match self {
73            TileMapBrushResourceError::Io(v) => {
74                write!(f, "A file load error has occurred {v:?}")
75            }
76            TileMapBrushResourceError::Visit(v) => {
77                write!(
78                    f,
79                    "An error that may occur due to version incompatibilities. {v:?}"
80                )
81            }
82        }
83    }
84}
85
86impl From<FileError> for TileMapBrushResourceError {
87    fn from(e: FileError) -> Self {
88        Self::Io(e)
89    }
90}
91
92impl From<VisitError> for TileMapBrushResourceError {
93    fn from(e: VisitError) -> Self {
94        Self::Visit(e)
95    }
96}
97
98/// Collection of additional data that can be stored in a brush for the purposes
99/// of tile map macros. The meaning of the data is determined by matching the UUID
100/// with the UUID of some macro or other user, and it is up to the user to determine
101/// the type of the resource.
102#[derive(Debug, Clone, Default, Reflect)]
103pub struct BrushMacroInstanceList(Vec<BrushMacroData>);
104
105impl BrushMacroInstanceList {
106    /// Iterate through the macro instance resources associated with the given UUID.
107    pub fn instances_with_uuid(&self, uuid: Uuid) -> impl Iterator<Item = &UntypedResource> {
108        self.0
109            .iter()
110            .filter(move |d| d.macro_id == uuid)
111            .filter_map(|d| d.settings.as_ref())
112    }
113}
114
115impl Deref for BrushMacroInstanceList {
116    type Target = Vec<BrushMacroData>;
117
118    fn deref(&self) -> &Self::Target {
119        &self.0
120    }
121}
122
123impl DerefMut for BrushMacroInstanceList {
124    fn deref_mut(&mut self) -> &mut Self::Target {
125        &mut self.0
126    }
127}
128
129/// Custom Visit implementation to prevent the visit from completely failing if
130/// it encounters data that cannot be visited.
131impl Visit for BrushMacroInstanceList {
132    fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult {
133        let mut region = visitor.enter_region(name)?;
134
135        let mut count = self.0.len() as u32;
136        count.visit("Count", &mut region)?;
137
138        if region.is_reading() {
139            self.clear();
140            for i in 0..(count as usize) {
141                let name = i.to_string();
142                // If one value fails to read, skip it and read the remaining data.
143                let mut value = BrushMacroData::default();
144                match value.visit(&name, &mut region) {
145                    Ok(()) => self.0.push(value),
146                    Err(err) => Log::err(format!("Failed to load brush tool data due to: {err}")),
147                }
148            }
149        } else {
150            for (i, value) in self.0.iter_mut().enumerate() {
151                let name = i.to_string();
152                value.visit(&name, &mut region)?;
153            }
154        }
155
156        Ok(())
157    }
158}
159
160/// A brush can have zero or more instances of a macro, and each instance
161/// has its own configuration data.
162#[derive(Debug, Default, Clone, Visit, Reflect)]
163pub struct BrushMacroData {
164    /// The UUID of the macro that owns this instance data.
165    /// This is used to identify the macro that knows the type of this data.
166    /// This same UUID will be shared by every data that is an instance
167    /// of the same macro.
168    pub macro_id: Uuid,
169    /// The human-readable name of the instance.
170    pub name: String,
171    /// The configuration of the macro, allowing its behaviour to be
172    /// controlled. The macro is responsible for determining the type of this
173    /// resource and its content. The macro may also choose to have no
174    /// configuration settings.
175    pub settings: Option<UntypedResource>,
176}
177
178/// A page of tiles within a brush. Having multiple pages allows a brush to be optimized
179/// for use in multiple contexts.
180#[derive(Default, Debug, Clone, Visit, Reflect)]
181pub struct TileMapBrushPage {
182    /// The tile that represents this page in the editor
183    pub icon: TileDefinitionHandle,
184    /// The tiles on this page, organized by position.
185    #[reflect(hidden)]
186    pub tiles: Tiles,
187}
188
189impl TileMapBrushPage {
190    /// The smallest Rect that contains all the tiles on this page.
191    pub fn bounding_rect(&self) -> OptionTileRect {
192        let mut result = OptionTileRect::default();
193        for pos in self.tiles.keys() {
194            result.push(*pos);
195        }
196        result
197    }
198    /// The tile definition handle at the given position.
199    pub fn find_tile_at_position(&self, position: Vector2<i32>) -> Option<TileDefinitionHandle> {
200        self.tiles.get(&position).copied()
201    }
202    /// The tile definition handles of the tiles at the given positions.
203    pub fn get_tiles<I: Iterator<Item = Vector2<i32>>>(&self, iter: I, tiles: &mut Tiles) {
204        for pos in iter {
205            if let Some(tile) = self.tiles.get(&pos).copied() {
206                tiles.insert(pos, tile);
207            }
208        }
209    }
210
211    /// Draw brush outline to the scene drawing context.
212    pub fn draw_outline(
213        &self,
214        ctx: &mut SceneDrawingContext,
215        position: Vector2<i32>,
216        world_transform: &Matrix4<f32>,
217        color: Color,
218    ) {
219        for (pos, _) in self.tiles.iter() {
220            draw_tile_outline(ctx, position + pos, world_transform, color);
221        }
222    }
223}
224
225fn draw_tile_outline(
226    ctx: &mut SceneDrawingContext,
227    position: Vector2<i32>,
228    world_transform: &Matrix4<f32>,
229    color: Color,
230) {
231    ctx.draw_rectangle(
232        0.5,
233        0.5,
234        Matrix4::new_translation(
235            &(position.cast::<f32>().to_homogeneous() + Vector3::new(0.5, 0.5, 0.0)),
236        ) * world_transform,
237        color,
238    );
239}
240
241/// Tile map brush is a set of tiles arranged in arbitrary shape, that can be used to draw on a tile
242/// map.
243#[derive(Default, Debug, Clone, Visit, Reflect, TypeUuidProvider)]
244#[type_uuid(id = "23ed39da-cb01-4181-a058-94dc77ecb4b2")]
245pub struct TileMapBrush {
246    /// The tile set used by this brush. This must match the tile set of any tile map that this
247    /// brush is used to edit.
248    pub tile_set: Option<TileSetResource>,
249    /// The set of pages contained in the brush
250    /// Each page is associated with 2D coordinates within a palette of brush pages.
251    /// This allows pages to be selected much like tiles are selected, and it allows
252    /// users to customize the organization of pages.
253    #[reflect(hidden)]
254    pub pages: TileGridMap<TileMapBrushPage>,
255    /// Untyped data stored in this brush for use by some macro.
256    #[visit(optional)]
257    pub macros: BrushMacroInstanceList,
258    /// A record of whether the brush has changed since last time it was saved.
259    #[reflect(hidden)]
260    #[visit(skip)]
261    pub change_flag: ChangeFlag,
262}
263
264impl TileMapBrush {
265    /// Return true after blocking to wait for the brush's tile set to load,
266    /// if the tile set loads successfully, or if the tile set has no brush.
267    /// Return false if any error occurs while trying to load the tile set.
268    pub fn block_until_tile_set_is_loaded(&self) -> bool {
269        let Some(tile_set) = self.tile_set.as_ref() else {
270            return true;
271        };
272        let tile_set = match block_on(tile_set.clone()) {
273            Ok(tile_set) => tile_set,
274            Err(e) => {
275                Log::err(format!("Tile set load failed! Reason: {e:?}"));
276                return false;
277            }
278        };
279        if tile_set.is_ok() {
280            true
281        } else {
282            Log::err("Tile set load failed!");
283            false
284        }
285    }
286    /// Return the tile set for this brush, blocking if the tile set is not yet
287    /// loaded. None is returned if this brush has no tile set or the tile set fails to load.
288    pub fn tile_set(&self) -> Option<TileSetResource> {
289        let tile_set = self.tile_set.as_ref()?;
290        let tile_set = match block_on(tile_set.clone()) {
291            Ok(tile_set) => tile_set,
292            Err(e) => {
293                Log::err(format!("Tile set load failed! Reason: {e:?}"));
294                return None;
295            }
296        };
297        if !tile_set.is_ok() {
298            Log::err("Tile set load failed!");
299            return None;
300        }
301        Some(tile_set)
302    }
303    /// True if there is a tile at the given position.
304    pub fn has_tile_at(&self, page: Vector2<i32>, tile: Vector2<i32>) -> bool {
305        let Some(page) = self.pages.get(&page) else {
306            return false;
307        };
308        page.tiles.contains_key(&tile)
309    }
310    /// True if there is a page at the given position.
311    pub fn has_page_at(&self, page: Vector2<i32>) -> bool {
312        self.pages.contains_key(&page)
313    }
314    /// The handle stored at the given position.
315    pub fn tile_redirect(&self, handle: TileDefinitionHandle) -> Option<TileDefinitionHandle> {
316        self.find_tile_at_position(TilePaletteStage::Tiles, handle.page(), handle.tile())
317    }
318    /// Returns bounding rectangle of pages in grid coordinates.
319    #[inline]
320    pub fn pages_bounds(&self) -> OptionTileRect {
321        let mut result = OptionTileRect::default();
322        for pos in self.pages.keys() {
323            result.push(*pos);
324        }
325        result
326    }
327    /// The handle of the tile that represents the page at the given position.
328    pub fn page_icon(&self, page: Vector2<i32>) -> Option<TileDefinitionHandle> {
329        self.pages.get(&page).map(|p| p.icon)
330    }
331    /// The bounds of the tiles on the given page.
332    pub fn tiles_bounds(&self, stage: TilePaletteStage, page: Vector2<i32>) -> OptionTileRect {
333        match stage {
334            TilePaletteStage::Tiles => {
335                let Some(page) = self.pages.get(&page) else {
336                    return OptionTileRect::default();
337                };
338                page.bounding_rect()
339            }
340            TilePaletteStage::Pages => self.pages_bounds(),
341        }
342    }
343
344    /// The handle of the tile at the given position, either the icon of a page or the tile stored at that position in the brush.
345    pub fn find_tile_at_position(
346        &self,
347        stage: TilePaletteStage,
348        page: Vector2<i32>,
349        position: Vector2<i32>,
350    ) -> Option<TileDefinitionHandle> {
351        match stage {
352            TilePaletteStage::Pages => self.pages.get(&position).map(|p| p.icon),
353            TilePaletteStage::Tiles => self
354                .pages
355                .get(&page)
356                .and_then(|p| p.find_tile_at_position(position)),
357        }
358    }
359
360    /// The tile definition handles of the tiles at the given positions on the given page.
361    pub fn get_tiles<I: Iterator<Item = Vector2<i32>>>(
362        &self,
363        stage: TilePaletteStage,
364        page: Vector2<i32>,
365        iter: I,
366        tiles: &mut Tiles,
367    ) {
368        match stage {
369            TilePaletteStage::Pages => {
370                for pos in iter {
371                    if let Some(handle) = self.pages.get(&pos).map(|p| p.icon) {
372                        tiles.insert(pos, handle);
373                    }
374                }
375            }
376            TilePaletteStage::Tiles => {
377                if let Some(page) = self.pages.get(&page) {
378                    page.get_tiles(iter, tiles);
379                }
380            }
381        }
382    }
383
384    /// Return true if this brush has no tile set.
385    pub fn is_missing_tile_set(&self) -> bool {
386        self.tile_set().is_none()
387    }
388
389    fn palette_render_loop_without_tile_set<F>(
390        &self,
391        stage: TilePaletteStage,
392        page: Vector2<i32>,
393        mut func: F,
394    ) where
395        F: FnMut(Vector2<i32>, TileRenderData),
396    {
397        match stage {
398            TilePaletteStage::Pages => {
399                for k in self.pages.keys() {
400                    func(*k, TileRenderData::missing_data());
401                }
402            }
403            TilePaletteStage::Tiles => {
404                let Some(page) = self.pages.get(&page) else {
405                    return;
406                };
407                for k in page.tiles.keys() {
408                    func(*k, TileRenderData::missing_data());
409                }
410            }
411        }
412    }
413
414    /// Loops through the tiles of the given page and finds the render data for each tile
415    /// in the tile set, then passes it to the given function.
416    pub fn palette_render_loop<F>(&self, stage: TilePaletteStage, page: Vector2<i32>, mut func: F)
417    where
418        F: FnMut(Vector2<i32>, TileRenderData),
419    {
420        let Some(tile_set) = self.tile_set() else {
421            self.palette_render_loop_without_tile_set(stage, page, func);
422            return;
423        };
424        let mut state = tile_set.state();
425        let Some(tile_set) = state.data() else {
426            self.palette_render_loop_without_tile_set(stage, page, func);
427            return;
428        };
429        match stage {
430            TilePaletteStage::Pages => {
431                for (k, p) in self.pages.iter() {
432                    let data = if p.icon.is_empty() {
433                        TileRenderData::empty()
434                    } else {
435                        tile_set
436                            .get_tile_render_data(p.icon.into())
437                            .unwrap_or_else(TileRenderData::missing_data)
438                    };
439                    func(*k, data);
440                }
441            }
442            TilePaletteStage::Tiles => {
443                let Some(page) = self.pages.get(&page) else {
444                    return;
445                };
446                for (k, &handle) in page.tiles.iter() {
447                    let data = if handle.is_empty() {
448                        TileRenderData::empty()
449                    } else {
450                        tile_set
451                            .get_tile_render_data(handle.into())
452                            .unwrap_or_else(TileRenderData::missing_data)
453                    };
454                    func(*k, data);
455                }
456            }
457        }
458    }
459
460    /// Return the `TileRenderData` needed to render the tile at the given position on the given page.
461    /// If there is no tile at that position or the tile set is missing or not loaded, then None is returned.
462    /// If there is a tile and a tile set, but the handle of the tile does not exist in the tile set,
463    /// then the rendering data for an error tile is returned using `TileRenderData::missing_tile()`.
464    pub fn get_tile_render_data(&self, position: ResourceTilePosition) -> Option<TileRenderData> {
465        let handle = self.redirect_handle(position)?;
466        if handle.is_empty() {
467            return Some(TileRenderData::empty());
468        }
469        let tile_set = self.tile_set()?;
470        let mut tile_set = tile_set.state();
471        let data = tile_set
472            .data()?
473            .get_tile_render_data(handle.into())
474            .unwrap_or_else(TileRenderData::missing_data);
475        Some(data)
476    }
477
478    /// The tiles of a brush are references to tiles in the tile set.
479    /// This method converts positions within the brush into the handle that points to the corresponding
480    /// tile definition within the tile set.
481    /// If this brush does not contain a reference at the given position, then None is returned.
482    pub fn redirect_handle(&self, position: ResourceTilePosition) -> Option<TileDefinitionHandle> {
483        match position.stage() {
484            TilePaletteStage::Tiles => {
485                let page = self.pages.get(&position.page())?;
486                page.tiles.get(&position.stage_position()).copied()
487            }
488            TilePaletteStage::Pages => self
489                .pages
490                .get(&position.stage_position())
491                .map(|page| page.icon),
492        }
493    }
494
495    /// The stamp element for the given position, if the tile in that cell is used
496    /// to create a stamp. The [`StampElement::handle`] refers to the location of the tile within the
497    /// tile set, while the [`StampElement::source`] refers to the location of the tile within
498    /// the brush.
499    pub fn stamp_element(&self, position: ResourceTilePosition) -> Option<StampElement> {
500        match position.stage() {
501            TilePaletteStage::Pages => self.redirect_handle(position).map(|handle| StampElement {
502                handle,
503                source: Some(position),
504            }),
505            TilePaletteStage::Tiles => {
506                let page = self.pages.get(&position.page())?;
507                Some(StampElement {
508                    handle: *page.tiles.get(&position.stage_position())?,
509                    source: Some(position),
510                })
511            }
512        }
513    }
514
515    /// The `TileMaterialBounds` taken from the tile set for the tile in the brush at the given position.
516    pub fn get_tile_bounds(&self, position: ResourceTilePosition) -> Option<TileMaterialBounds> {
517        let handle = self.redirect_handle(position)?;
518        self.tile_set
519            .as_ref()?
520            .state()
521            .data()?
522            .get_tile_bounds(handle.into())
523    }
524
525    /// Returns true if the brush is unoccupied at the given position.
526    pub fn is_free_at(&self, position: ResourceTilePosition) -> bool {
527        match position.stage() {
528            TilePaletteStage::Pages => !self.pages.contains_key(&position.stage_position()),
529            TilePaletteStage::Tiles => !self
530                .pages
531                .get(&position.page())
532                .map(|p| p.tiles.contains_key(&position.stage_position()))
533                .unwrap_or_default(),
534        }
535    }
536
537    /// Load a tile map brush resource from the specific file path.
538    pub async fn from_file(
539        path: &Path,
540        resource_manager: ResourceManager,
541        io: &dyn ResourceIo,
542    ) -> Result<Self, TileMapBrushResourceError> {
543        let bytes = io.load_file(path).await?;
544        let mut visitor = Visitor::load_from_memory(&bytes)?;
545        visitor.blackboard.register(Arc::new(resource_manager));
546        let mut tile_map_brush = Self::default();
547        tile_map_brush.visit("TileMapBrush", &mut visitor)?;
548        Ok(tile_map_brush)
549    }
550
551    fn save(&mut self, path: &Path) -> Result<(), Box<dyn Error>> {
552        let mut visitor = Visitor::new();
553        self.visit("TileMapBrush", &mut visitor)?;
554        visitor.save_ascii_to_file(path)?;
555        Ok(())
556    }
557}
558
559impl ResourceData for TileMapBrush {
560    fn type_uuid(&self) -> Uuid {
561        <Self as TypeUuidProvider>::type_uuid()
562    }
563
564    fn save(&mut self, path: &Path) -> Result<(), Box<dyn Error>> {
565        self.save(path)
566    }
567
568    fn can_be_saved(&self) -> bool {
569        true
570    }
571
572    fn try_clone_box(&self) -> Option<Box<dyn ResourceData>> {
573        Some(Box::new(self.clone()))
574    }
575}
576
577/// Standard tile map brush loader.
578pub struct TileMapBrushLoader {
579    /// The resource manager to use to load the brush's tile set.
580    pub resource_manager: ResourceManager,
581}
582
583impl ResourceLoader for TileMapBrushLoader {
584    fn extensions(&self) -> &[&str] {
585        &["tile_map_brush"]
586    }
587
588    fn is_native_extension(&self, ext: &str) -> bool {
589        fyrox_core::cmp_strings_case_insensitive(ext, "tile_map_brush")
590    }
591
592    fn data_type_uuid(&self) -> Uuid {
593        <TileMapBrush as TypeUuidProvider>::type_uuid()
594    }
595
596    fn load(&self, path: PathBuf, io: Arc<dyn ResourceIo>) -> BoxedLoaderFuture {
597        let resource_manager = self.resource_manager.clone();
598        Box::pin(async move {
599            let tile_map_brush = TileMapBrush::from_file(&path, resource_manager, io.as_ref())
600                .await
601                .map_err(LoadError::new)?;
602            Ok(LoaderPayload::new(tile_map_brush))
603        })
604    }
605}
606
607/// An alias to `Resource<TileMapBrush>`.
608pub type TileMapBrushResource = Resource<TileMapBrush>;