diff --git a/editor/resources/nine_slice.png b/editor/resources/nine_slice.png new file mode 100644 index 000000000..f28c085f1 Binary files /dev/null and b/editor/resources/nine_slice.png differ diff --git a/editor/src/plugins/tilemap/mod.rs b/editor/src/plugins/tilemap/mod.rs index 706c5d567..6df0b503a 100644 --- a/editor/src/plugins/tilemap/mod.rs +++ b/editor/src/plugins/tilemap/mod.rs @@ -81,6 +81,9 @@ pub enum DrawingMode { RectFill { click_grid_position: Option>, }, + NineSlice { + click_grid_position: Option>, + }, } struct InteractionContext { @@ -170,6 +173,9 @@ impl InteractionMode for TileMapInteractionMode { } | DrawingMode::Pick { ref mut click_grid_position, + } + | DrawingMode::NineSlice { + ref mut click_grid_position, } => { *click_grid_position = Some(grid_coord); } @@ -237,7 +243,16 @@ impl InteractionMode for TileMapInteractionMode { ); } } - + DrawingMode::NineSlice { + click_grid_position, + } => { + if let Some(click_grid_position) = click_grid_position { + tile_map.tiles.nine_slice( + Rect::from_points(grid_coord, click_grid_position), + &brush, + ) + } + } _ => (), } } @@ -363,6 +378,9 @@ impl InteractionMode for TileMapInteractionMode { } | DrawingMode::RectFill { click_grid_position, + } + | DrawingMode::NineSlice { + click_grid_position, } => { if self.interaction_context.is_some() { if let Some(click_grid_position) = click_grid_position { @@ -418,6 +436,18 @@ impl InteractionMode for TileMapInteractionMode { } } } + DrawingMode::NineSlice { + click_grid_position, + } => { + if self.interaction_context.is_some() { + if let Some(click_grid_position) = click_grid_position { + tile_map.overlay_tiles.nine_slice( + Rect::from_points(self.brush_position, click_grid_position), + &brush, + ); + } + } + } } } diff --git a/editor/src/plugins/tilemap/panel.rs b/editor/src/plugins/tilemap/panel.rs index ba1c1f5dc..dd8924f72 100644 --- a/editor/src/plugins/tilemap/panel.rs +++ b/editor/src/plugins/tilemap/panel.rs @@ -62,6 +62,7 @@ pub struct TileMapPanel { flood_fill_button: Handle, pick_button: Handle, rect_fill_button: Handle, + nine_slice_button: Handle, } fn generate_tiles( @@ -208,6 +209,14 @@ impl TileMapPanel { "Fill the rectangle using the current brush.", Some(0), ); + let nine_slice_button = make_drawing_mode_button( + ctx, + width, + height, + load_image(include_bytes!("../../../resources/nine_slice.png")), + "Draw rectangles with fixed corners, but stretchable sides.", + Some(0), + ); let drawing_modes_panel = WrapPanelBuilder::new( WidgetBuilder::new() @@ -216,7 +225,8 @@ impl TileMapPanel { .with_child(erase_button) .with_child(flood_fill_button) .with_child(pick_button) - .with_child(rect_fill_button), + .with_child(rect_fill_button) + .with_child(nine_slice_button), ) .with_orientation(Orientation::Horizontal) .build(ctx); @@ -269,6 +279,7 @@ impl TileMapPanel { flood_fill_button, pick_button, rect_fill_button, + nine_slice_button, } } @@ -432,6 +443,10 @@ impl TileMapPanel { interaction_mode.drawing_mode = DrawingMode::Pick { click_grid_position: Default::default(), }; + } else if message.destination() == self.nine_slice_button { + interaction_mode.drawing_mode = DrawingMode::NineSlice { + click_grid_position: Default::default(), + }; } else if message.destination() == self.edit { sender.send(Message::SetInteractionMode( TileMapInteractionMode::type_uuid(), @@ -495,6 +510,9 @@ impl TileMapPanel { DrawingMode::RectFill { .. } => { highlight_all_except(self.rect_fill_button, &buttons, true, ui); } + DrawingMode::NineSlice { .. } => { + highlight_all_except(self.nine_slice_button, &buttons, true, ui); + } } } } diff --git a/fyrox-impl/src/scene/tilemap/mod.rs b/fyrox-impl/src/scene/tilemap/mod.rs index 37502045e..47a2e7f13 100644 --- a/fyrox-impl/src/scene/tilemap/mod.rs +++ b/fyrox-impl/src/scene/tilemap/mod.rs @@ -34,6 +34,73 @@ use crate::{ use fxhash::{FxHashMap, FxHashSet}; use std::ops::{Deref, DerefMut}; +struct BresenhamLineIter { + dx: i32, + dy: i32, + x: i32, + y: i32, + error: i32, + end_x: i32, + is_steep: bool, + y_step: i32, +} + +impl BresenhamLineIter { + fn new(start: Vector2, end: Vector2) -> BresenhamLineIter { + let (mut x0, mut y0) = (start.x, start.y); + let (mut x1, mut y1) = (end.x, end.y); + + let is_steep = (y1 - y0).abs() > (x1 - x0).abs(); + if is_steep { + std::mem::swap(&mut x0, &mut y0); + std::mem::swap(&mut x1, &mut y1); + } + + if x0 > x1 { + std::mem::swap(&mut x0, &mut x1); + std::mem::swap(&mut y0, &mut y1); + } + + let dx = x1 - x0; + + BresenhamLineIter { + dx, + dy: (y1 - y0).abs(), + x: x0, + y: y0, + error: dx / 2, + end_x: x1, + is_steep, + y_step: if y0 < y1 { 1 } else { -1 }, + } + } +} + +impl Iterator for BresenhamLineIter { + type Item = Vector2; + + fn next(&mut self) -> Option> { + if self.x > self.end_x { + None + } else { + let ret = if self.is_steep { + Vector2::new(self.y, self.x) + } else { + Vector2::new(self.x, self.y) + }; + + self.x += 1; + self.error -= self.dy; + if self.error < 0 { + self.y += self.y_step; + self.error += self.dx; + } + + Some(ret) + } + } +} + /// Tile is a base block of a tile map. It has a position and a handle of tile definition, stored /// in the respective tile set. #[derive(Clone, Reflect, Default, Debug, PartialEq, Visit, ComponentProvider, TypeUuidProvider)] @@ -216,6 +283,109 @@ impl Tiles { } } } + + /// Draw a line from a point to point. + #[inline] + pub fn line( + &mut self, + from: Vector2, + to: Vector2, + definition_handle: TileDefinitionHandle, + ) { + for position in BresenhamLineIter::new(from, to) { + self.insert(Tile { + position, + definition_handle, + }); + } + } + + /// Fills in a rectangle using special brush with 3x3 tiles. It puts + /// corner tiles in the respective corners of the target rectangle and draws lines between each + /// corner using middle tiles. + #[inline] + pub fn nine_slice(&mut self, rect: Rect, brush: &TileMapBrush) { + let brush_rect = brush.bounding_rect(); + + // Place corners first. + for (corner_position, actual_corner_position) in [ + (Vector2::new(0, 0), rect.left_top_corner()), + (Vector2::new(2, 0), rect.right_top_corner()), + (Vector2::new(2, 2), rect.right_bottom_corner()), + (Vector2::new(0, 2), rect.left_bottom_corner()), + ] { + if let Some(tile) = brush + .tiles + .iter() + .find(|tile| tile.local_position - brush_rect.position == corner_position) + { + self.insert(Tile { + position: actual_corner_position, + definition_handle: tile.definition_handle, + }); + } + } + + // Fill gaps. + for (brush_tile_position, (begin, end)) in [ + ( + Vector2::new(0, 1), + ( + Vector2::new(rect.position.x, rect.position.y + 1), + Vector2::new(rect.position.x, rect.position.y + rect.size.y - 1), + ), + ), + ( + Vector2::new(1, 0), + ( + Vector2::new(rect.position.x + 1, rect.position.y), + Vector2::new(rect.position.x + rect.size.x - 1, rect.position.y), + ), + ), + ( + Vector2::new(2, 1), + ( + Vector2::new(rect.position.x + rect.size.x, rect.position.y + 1), + Vector2::new( + rect.position.x + rect.size.x, + rect.position.y + rect.size.y - 1, + ), + ), + ), + ( + Vector2::new(1, 2), + ( + Vector2::new(rect.position.x + 1, rect.position.y + rect.size.y), + Vector2::new( + rect.position.x + rect.size.x - 1, + rect.position.y + rect.size.y, + ), + ), + ), + ] { + if let Some(tile) = brush + .tiles + .iter() + .find(|tile| tile.local_position - brush_rect.position == brush_tile_position) + { + self.line(begin, end, tile.definition_handle); + } + } + + if let Some(center_tile) = brush + .tiles + .iter() + .find(|tile| tile.local_position - brush_rect.position == Vector2::new(1, 1)) + { + self.flood_fill( + rect.center(), + &TileMapBrush { + // TODO: Remove alloc. + tiles: vec![center_tile.clone()], + }, + ); + } + } } /// Tile map is a 2D "image", made out of a small blocks called tiles. Tile maps used in 2D games to