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 crate::{
31    asset::{
32        io::ResourceIo,
33        loader::{BoxedLoaderFuture, LoaderPayload, ResourceLoader},
34        manager::ResourceManager,
35        state::LoadError,
36        Resource, ResourceData,
37    },
38    core::{
39        algebra::{Matrix4, Vector2, Vector3},
40        color::Color,
41        io::FileLoadError,
42        reflect::prelude::*,
43        type_traits::prelude::*,
44        visitor::prelude::*,
45    },
46    scene::debug::SceneDrawingContext,
47};
48use std::{
49    error::Error,
50    fmt::{Display, Formatter},
51    path::{Path, PathBuf},
52    sync::Arc,
53};
54
55use super::*;
56
57/// An error that may occur during tile map brush resource loading.
58#[derive(Debug)]
59pub enum TileMapBrushResourceError {
60    /// An i/o error has occurred.
61    Io(FileLoadError),
62
63    /// An error that may occur due to version incompatibilities.
64    Visit(VisitError),
65}
66
67impl Display for TileMapBrushResourceError {
68    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
69        match self {
70            TileMapBrushResourceError::Io(v) => {
71                write!(f, "A file load error has occurred {v:?}")
72            }
73            TileMapBrushResourceError::Visit(v) => {
74                write!(
75                    f,
76                    "An error that may occur due to version incompatibilities. {v:?}"
77                )
78            }
79        }
80    }
81}
82
83impl From<FileLoadError> for TileMapBrushResourceError {
84    fn from(e: FileLoadError) -> Self {
85        Self::Io(e)
86    }
87}
88
89impl From<VisitError> for TileMapBrushResourceError {
90    fn from(e: VisitError) -> Self {
91        Self::Visit(e)
92    }
93}
94
95/// A page of tiles within a brush. Having multiple pages allows a brush to be optimized
96/// for use in multiple contexts.
97#[derive(Default, Debug, Clone, Visit, Reflect)]
98pub struct TileMapBrushPage {
99    /// The tile that represents this page in the editor
100    pub icon: TileDefinitionHandle,
101    /// The tiles on this page, organized by position.
102    #[reflect(hidden)]
103    pub tiles: Tiles,
104}
105
106impl TileSource for TileMapBrushPage {
107    fn transformation(&self) -> OrthoTransformation {
108        OrthoTransformation::default()
109    }
110    fn get_at(&self, position: Vector2<i32>) -> Option<TileDefinitionHandle> {
111        self.tiles.get(&position).copied()
112    }
113}
114
115impl TileMapBrushPage {
116    /// The smallest Rect that contains all the tiles on this page.
117    pub fn bounding_rect(&self) -> OptionTileRect {
118        let mut result = OptionTileRect::default();
119        for pos in self.tiles.keys() {
120            result.push(*pos);
121        }
122        result
123    }
124    /// The tile definition handle at the given position.
125    pub fn find_tile_at_position(&self, position: Vector2<i32>) -> Option<TileDefinitionHandle> {
126        self.tiles.get(&position).copied()
127    }
128    /// The tile definition handles of the tiles at the given positions.
129    pub fn get_tiles<I: Iterator<Item = Vector2<i32>>>(&self, iter: I, tiles: &mut Tiles) {
130        for pos in iter {
131            if let Some(tile) = self.tiles.get(&pos).copied() {
132                tiles.insert(pos, tile);
133            }
134        }
135    }
136
137    /// Draw brush outline to the scene drawing context.
138    pub fn draw_outline(
139        &self,
140        ctx: &mut SceneDrawingContext,
141        position: Vector2<i32>,
142        world_transform: &Matrix4<f32>,
143        color: Color,
144    ) {
145        for (pos, _) in self.tiles.iter() {
146            draw_tile_outline(ctx, position + pos, world_transform, color);
147        }
148    }
149}
150
151fn draw_tile_outline(
152    ctx: &mut SceneDrawingContext,
153    position: Vector2<i32>,
154    world_transform: &Matrix4<f32>,
155    color: Color,
156) {
157    ctx.draw_rectangle(
158        0.5,
159        0.5,
160        Matrix4::new_translation(
161            &(position.cast::<f32>().to_homogeneous() + Vector3::new(0.5, 0.5, 0.0)),
162        ) * world_transform,
163        color,
164    );
165}
166
167/// Tile map brush is a set of tiles arranged in arbitrary shape, that can be used to draw on a tile
168/// map.
169#[derive(Default, Debug, Clone, Visit, Reflect, TypeUuidProvider)]
170#[type_uuid(id = "23ed39da-cb01-4181-a058-94dc77ecb4b2")]
171pub struct TileMapBrush {
172    /// The tile set used by this brush. This must match the tile set of any tile map that this
173    /// brush is used to edit.
174    pub tile_set: Option<TileSetResource>,
175    /// The set of pages contained in the brush
176    /// Each page is associated with 2D coordinates within a palette of brush pages.
177    /// This allows pages to be selected much like tiles are selected, and it allows
178    /// users to customize the organization of pages.
179    #[reflect(hidden)]
180    pub pages: TileGridMap<TileMapBrushPage>,
181    /// A count of changes since last save. New changes add +1. Reverting to previous
182    /// states add -1. Reverting to a state before the last save can result in negative
183    /// values. Saving is unnecessary whenever this value is 0.
184    #[reflect(hidden)]
185    #[visit(skip)]
186    pub change_count: ChangeFlag,
187}
188
189impl TileMapBrush {
190    /// True if there is a tile at the given position.
191    pub fn has_tile_at(&self, page: Vector2<i32>, tile: Vector2<i32>) -> bool {
192        let Some(page) = self.pages.get(&page) else {
193            return false;
194        };
195        page.tiles.contains_key(&tile)
196    }
197    /// True if there is a page at the given position.
198    pub fn has_page_at(&self, page: Vector2<i32>) -> bool {
199        self.pages.contains_key(&page)
200    }
201    /// The handle stored at the given position.
202    pub fn tile_redirect(&self, handle: TileDefinitionHandle) -> Option<TileDefinitionHandle> {
203        self.find_tile_at_position(TilePaletteStage::Tiles, handle.page(), handle.tile())
204    }
205    /// Returns bounding rectangle of pages in grid coordinates.
206    #[inline]
207    pub fn pages_bounds(&self) -> OptionTileRect {
208        let mut result = OptionTileRect::default();
209        for pos in self.pages.keys() {
210            result.push(*pos);
211        }
212        result
213    }
214    /// The handle of the tile that represents the page at the given position.
215    pub fn page_icon(&self, page: Vector2<i32>) -> Option<TileDefinitionHandle> {
216        self.pages.get(&page).map(|p| p.icon)
217    }
218    /// The bounds of the tiles on the given page.
219    pub fn tiles_bounds(&self, stage: TilePaletteStage, page: Vector2<i32>) -> OptionTileRect {
220        match stage {
221            TilePaletteStage::Tiles => {
222                let Some(page) = self.pages.get(&page) else {
223                    return OptionTileRect::default();
224                };
225                page.bounding_rect()
226            }
227            TilePaletteStage::Pages => self.pages_bounds(),
228        }
229    }
230
231    /// 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.
232    pub fn find_tile_at_position(
233        &self,
234        stage: TilePaletteStage,
235        page: Vector2<i32>,
236        position: Vector2<i32>,
237    ) -> Option<TileDefinitionHandle> {
238        match stage {
239            TilePaletteStage::Pages => self.pages.get(&position).map(|p| p.icon),
240            TilePaletteStage::Tiles => self
241                .pages
242                .get(&page)
243                .and_then(|p| p.find_tile_at_position(position)),
244        }
245    }
246
247    /// The tile definition handles of the tiles at the given positions on the given page.
248    pub fn get_tiles<I: Iterator<Item = Vector2<i32>>>(
249        &self,
250        stage: TilePaletteStage,
251        page: Vector2<i32>,
252        iter: I,
253        tiles: &mut Tiles,
254    ) {
255        match stage {
256            TilePaletteStage::Pages => {
257                for pos in iter {
258                    if let Some(handle) = self.pages.get(&pos).map(|p| p.icon) {
259                        tiles.insert(pos, handle);
260                    }
261                }
262            }
263            TilePaletteStage::Tiles => {
264                if let Some(page) = self.pages.get(&page) {
265                    page.get_tiles(iter, tiles);
266                }
267            }
268        }
269    }
270
271    /// Return true if this brush has no tile set.
272    pub fn is_missing_tile_set(&self) -> bool {
273        self.tile_set.is_none()
274    }
275
276    fn palette_render_loop_without_tile_set<F>(
277        &self,
278        stage: TilePaletteStage,
279        page: Vector2<i32>,
280        mut func: F,
281    ) where
282        F: FnMut(Vector2<i32>, TileRenderData),
283    {
284        match stage {
285            TilePaletteStage::Pages => {
286                for k in self.pages.keys() {
287                    func(*k, TileRenderData::missing_data());
288                }
289            }
290            TilePaletteStage::Tiles => {
291                let Some(page) = self.pages.get(&page) else {
292                    return;
293                };
294                for k in page.tiles.keys() {
295                    func(*k, TileRenderData::missing_data());
296                }
297            }
298        }
299    }
300
301    /// Loops through the tiles of the given page and finds the render data for each tile
302    /// in the tile set, then passes it to the given function.
303    pub fn palette_render_loop<F>(&self, stage: TilePaletteStage, page: Vector2<i32>, mut func: F)
304    where
305        F: FnMut(Vector2<i32>, TileRenderData),
306    {
307        let Some(tile_set) = self.tile_set.as_ref() else {
308            self.palette_render_loop_without_tile_set(stage, page, func);
309            return;
310        };
311        let mut state = tile_set.state();
312        let Some(tile_set) = state.data() else {
313            self.palette_render_loop_without_tile_set(stage, page, func);
314            return;
315        };
316        match stage {
317            TilePaletteStage::Pages => {
318                for (k, p) in self.pages.iter() {
319                    let data = tile_set
320                        .get_tile_render_data(p.icon.into())
321                        .unwrap_or_else(TileRenderData::missing_data);
322                    func(*k, data);
323                }
324            }
325            TilePaletteStage::Tiles => {
326                let Some(page) = self.pages.get(&page) else {
327                    return;
328                };
329                for (k, &handle) in page.tiles.iter() {
330                    let data = tile_set
331                        .get_tile_render_data(handle.into())
332                        .unwrap_or_else(TileRenderData::missing_data);
333                    func(*k, data);
334                }
335            }
336        }
337    }
338
339    /// Return the `TileRenderData` needed to render the tile at the given position on the given page.
340    /// If there is no tile at that position or the tile set is missing or not loaded, then None is returned.
341    /// If there is a tile and a tile set, but the handle of the tile does not exist in the tile set,
342    /// then the rendering data for an error tile is returned using `TileRenderData::missing_tile()`.
343    pub fn get_tile_render_data(&self, position: ResourceTilePosition) -> Option<TileRenderData> {
344        let handle = self.redirect_handle(position)?;
345        let mut tile_set = self.tile_set.as_ref()?.state();
346        let data = tile_set
347            .data()?
348            .get_tile_render_data(handle.into())
349            .unwrap_or_else(TileRenderData::missing_data);
350        Some(data)
351    }
352
353    /// The tiles of a brush are references to tiles in the tile set.
354    /// This method converts positions within the brush into the handle that points to the corresponding
355    /// tile definition within the tile set.
356    /// If this brush does not contain a reference at the given position, then None is returned.
357    pub fn redirect_handle(&self, position: ResourceTilePosition) -> Option<TileDefinitionHandle> {
358        match position.stage() {
359            TilePaletteStage::Tiles => {
360                let page = self.pages.get(&position.page())?;
361                page.tiles.get_at(position.stage_position())
362            }
363            TilePaletteStage::Pages => self
364                .pages
365                .get(&position.stage_position())
366                .map(|page| page.icon),
367        }
368    }
369
370    /// The `TileMaterialBounds` taken from the tile set for the tile in the brush at the given position.
371    pub fn get_tile_bounds(&self, position: ResourceTilePosition) -> Option<TileMaterialBounds> {
372        let handle = self.redirect_handle(position)?;
373        self.tile_set
374            .as_ref()?
375            .state()
376            .data()?
377            .get_tile_bounds(handle.into())
378    }
379
380    /// Load a tile map brush resource from the specific file path.
381    pub async fn from_file(
382        path: &Path,
383        resource_manager: ResourceManager,
384        io: &dyn ResourceIo,
385    ) -> Result<Self, TileMapBrushResourceError> {
386        let bytes = io.load_file(path).await?;
387        let mut visitor = Visitor::load_from_memory(&bytes)?;
388        visitor.blackboard.register(Arc::new(resource_manager));
389        let mut tile_map_brush = Self::default();
390        tile_map_brush.visit("TileMapBrush", &mut visitor)?;
391        Ok(tile_map_brush)
392    }
393
394    fn save(&mut self, path: &Path) -> Result<(), Box<dyn Error>> {
395        let mut visitor = Visitor::new();
396        self.visit("TileMapBrush", &mut visitor)?;
397        visitor.save_binary(path)?;
398        Ok(())
399    }
400}
401
402impl ResourceData for TileMapBrush {
403    fn type_uuid(&self) -> Uuid {
404        <Self as TypeUuidProvider>::type_uuid()
405    }
406
407    fn save(&mut self, path: &Path) -> Result<(), Box<dyn Error>> {
408        self.save(path)
409    }
410
411    fn can_be_saved(&self) -> bool {
412        true
413    }
414}
415
416/// Standard tile map brush loader.
417pub struct TileMapBrushLoader {
418    /// The resource manager to use to load the brush's tile set.
419    pub resource_manager: ResourceManager,
420}
421
422impl ResourceLoader for TileMapBrushLoader {
423    fn extensions(&self) -> &[&str] {
424        &["tile_map_brush"]
425    }
426
427    fn data_type_uuid(&self) -> Uuid {
428        <TileMapBrush as TypeUuidProvider>::type_uuid()
429    }
430
431    fn load(&self, path: PathBuf, io: Arc<dyn ResourceIo>) -> BoxedLoaderFuture {
432        let resource_manager = self.resource_manager.clone();
433        Box::pin(async move {
434            let tile_map_brush = TileMapBrush::from_file(&path, resource_manager, io.as_ref())
435                .await
436                .map_err(LoadError::new)?;
437            Ok(LoaderPayload::new(tile_map_brush))
438        })
439    }
440}
441
442/// An alias to `Resource<TileMapBrush>`.
443pub type TileMapBrushResource = Resource<TileMapBrush>;