rustapi/editor_tools/
tile_fill.rs1use 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}