Skip to main content

rustapi/editor_tools/
tile_fill.rs

1use crate::docks::tiles_editor_undo::TileEditorUndoAtom;
2use crate::prelude::*;
3
4pub struct TileFillTool {
5    id: TheId,
6    before_tile: Option<rusterix::Tile>,
7    before_snapshot: Option<(PixelEditingContext, rusterix::Texture)>,
8}
9
10impl EditorTool for TileFillTool {
11    fn new() -> Self
12    where
13        Self: Sized,
14    {
15        Self {
16            id: TheId::named_with_id("Tile Fill Tool", Uuid::new_v4()),
17            before_tile: None,
18            before_snapshot: None,
19        }
20    }
21
22    fn id(&self) -> TheId {
23        self.id.clone()
24    }
25
26    fn info(&self) -> String {
27        "Fill Tool (F). Click to flood fill connected pixels; respects active selection."
28            .to_string()
29    }
30
31    fn icon_name(&self) -> String {
32        "paint-bucket".to_string()
33    }
34
35    fn rgba_view_mode(&self) -> Option<TheRGBAViewMode> {
36        Some(TheRGBAViewMode::TileEditor)
37    }
38
39    fn accel(&self) -> Option<char> {
40        Some('F')
41    }
42
43    fn handle_event(
44        &mut self,
45        event: &TheEvent,
46        ui: &mut TheUI,
47        ctx: &mut TheContext,
48        project: &mut Project,
49        server_ctx: &mut ServerContext,
50    ) -> bool {
51        if let TheEvent::TileEditorClicked(id, coord) = event
52            && id.name == "Tile Editor Dock RGBA Layout View"
53            && self.fill_from(*coord, ui, ctx, project, server_ctx)
54        {
55            ctx.ui.send(TheEvent::Custom(
56                TheId::named("Tile Editor Undo Available"),
57                TheValue::Empty,
58            ));
59            return true;
60        }
61        false
62    }
63
64    fn get_undo_atom(&mut self, project: &Project) -> Option<Box<dyn std::any::Any>> {
65        if let Some(before) = self.before_tile.take() {
66            if let Some(tile) = project.tiles.get(&before.id) {
67                if !tile.textures.is_empty() {
68                    let after = tile.clone();
69                    let atom = TileEditorUndoAtom::TileEdit(before.id, before, after);
70                    return Some(Box::new(atom));
71                }
72            }
73            return None;
74        }
75
76        if let Some((editing_ctx, before)) = self.before_snapshot.take() {
77            if let Some(after) = project.get_editing_texture(&editing_ctx) {
78                let atom = TileEditorUndoAtom::TextureEdit(editing_ctx, before, after.clone());
79                return Some(Box::new(atom));
80            }
81        }
82        None
83    }
84}
85
86impl TileFillTool {
87    fn fill_from(
88        &mut self,
89        pos: Vec2<i32>,
90        ui: &mut TheUI,
91        ctx: &mut TheContext,
92        project: &mut Project,
93        server_ctx: &mut ServerContext,
94    ) -> bool {
95        let editing_ctx = server_ctx.editing_ctx;
96
97        if matches!(editing_ctx, PixelEditingContext::AvatarFrame(..))
98            && server_ctx.avatar_anchor_slot != AvatarAnchorEditSlot::None
99        {
100            return false;
101        }
102
103        let Some(editor) = ui.get_rgba_layout("Tile Editor Dock RGBA Layout") else {
104            return false;
105        };
106        let Some(rgba_view) = editor.rgba_view_mut().as_rgba_view() else {
107            return false;
108        };
109
110        if rgba_view.has_paste_preview() {
111            return false;
112        }
113
114        let selection = rgba_view.selection();
115        if !selection.is_empty() && !selection.contains(&(pos.x, pos.y)) {
116            return false;
117        }
118
119        let Some(fill_color) = (if ui.shift {
120            Some([0, 0, 0, 0])
121        } else {
122            editing_ctx.get_draw_color(
123                &project.palette,
124                server_ctx.palette_opacity,
125                server_ctx.body_marker_color,
126            )
127        }) else {
128            return false;
129        };
130
131        self.before_tile = None;
132        self.before_snapshot = None;
133        match editing_ctx {
134            PixelEditingContext::Tile(tile_id, _) => {
135                if let Some(tile) = project.tiles.get(&tile_id) {
136                    self.before_tile = Some(tile.clone());
137                }
138            }
139            _ => {
140                if let Some(texture) = project.get_editing_texture(&editing_ctx) {
141                    self.before_snapshot = Some((editing_ctx, texture.clone()));
142                }
143            }
144        }
145
146        let Some(texture) = project.get_editing_texture_mut(&editing_ctx) else {
147            return false;
148        };
149        let width = texture.width as i32;
150        let height = texture.height as i32;
151        if pos.x < 0 || pos.y < 0 || pos.x >= width || pos.y >= height {
152            return false;
153        }
154
155        let target = texture.get_pixel(pos.x as u32, pos.y as u32);
156        if target == fill_color {
157            self.before_tile = None;
158            self.before_snapshot = None;
159            return false;
160        }
161
162        let mut stack = vec![(pos.x, pos.y)];
163        let mut visited = vec![false; texture.width * texture.height];
164        let mut changed = false;
165
166        while let Some((x, y)) = stack.pop() {
167            if x < 0 || y < 0 || x >= width || y >= height {
168                continue;
169            }
170            if !selection.is_empty() && !selection.contains(&(x, y)) {
171                continue;
172            }
173
174            let idx = y as usize * texture.width + x as usize;
175            if visited[idx] {
176                continue;
177            }
178            visited[idx] = true;
179
180            if texture.get_pixel(x as u32, y as u32) != target {
181                continue;
182            }
183
184            texture.set_pixel(x as u32, y as u32, fill_color);
185            changed = true;
186
187            stack.push((x + 1, y));
188            stack.push((x - 1, y));
189            stack.push((x, y + 1));
190            stack.push((x, y - 1));
191        }
192
193        if !changed {
194            self.before_tile = None;
195            self.before_snapshot = None;
196            return false;
197        }
198
199        texture.generate_normals(true);
200
201        match editing_ctx {
202            PixelEditingContext::Tile(tile_id, _) => {
203                ctx.ui.send(TheEvent::Custom(
204                    TheId::named("Tile Updated"),
205                    TheValue::Id(tile_id),
206                ));
207                ctx.ui.send(TheEvent::Custom(
208                    TheId::named("Update Tilepicker"),
209                    TheValue::Empty,
210                ));
211            }
212            PixelEditingContext::AvatarFrame(..) => {
213                ctx.ui.send(TheEvent::Custom(
214                    TheId::named("Editing Texture Updated"),
215                    TheValue::Empty,
216                ));
217            }
218            PixelEditingContext::None => {}
219        }
220
221        true
222    }
223}