eldiron-creator 0.9.3

A game creator for classical RPGs.
Documentation
use crate::docks::tiles_editor_undo::TileEditorUndoAtom;
use crate::prelude::*;

pub struct TileFillTool {
    id: TheId,
    before_tile: Option<rusterix::Tile>,
    before_snapshot: Option<(PixelEditingContext, rusterix::Texture)>,
}

impl EditorTool for TileFillTool {
    fn new() -> Self
    where
        Self: Sized,
    {
        Self {
            id: TheId::named_with_id("Tile Fill Tool", Uuid::new_v4()),
            before_tile: None,
            before_snapshot: None,
        }
    }

    fn id(&self) -> TheId {
        self.id.clone()
    }

    fn info(&self) -> String {
        "Fill Tool (F). Click to flood fill connected pixels; respects active selection."
            .to_string()
    }

    fn icon_name(&self) -> String {
        "paint-bucket".to_string()
    }

    fn rgba_view_mode(&self) -> Option<TheRGBAViewMode> {
        Some(TheRGBAViewMode::TileEditor)
    }

    fn accel(&self) -> Option<char> {
        Some('F')
    }

    fn handle_event(
        &mut self,
        event: &TheEvent,
        ui: &mut TheUI,
        ctx: &mut TheContext,
        project: &mut Project,
        server_ctx: &mut ServerContext,
    ) -> bool {
        if let TheEvent::TileEditorClicked(id, coord) = event
            && id.name == "Tile Editor Dock RGBA Layout View"
            && self.fill_from(*coord, ui, ctx, project, server_ctx)
        {
            ctx.ui.send(TheEvent::Custom(
                TheId::named("Tile Editor Undo Available"),
                TheValue::Empty,
            ));
            return true;
        }
        false
    }

    fn get_undo_atom(&mut self, project: &Project) -> Option<Box<dyn std::any::Any>> {
        if let Some(before) = self.before_tile.take() {
            if let Some(tile) = project.tiles.get(&before.id) {
                if !tile.textures.is_empty() {
                    let after = tile.clone();
                    let atom = TileEditorUndoAtom::TileEdit(before.id, before, after);
                    return Some(Box::new(atom));
                }
            }
            return None;
        }

        if let Some((editing_ctx, before)) = self.before_snapshot.take() {
            if let Some(after) = project.get_editing_texture(&editing_ctx) {
                let atom = TileEditorUndoAtom::TextureEdit(editing_ctx, before, after.clone());
                return Some(Box::new(atom));
            }
        }
        None
    }
}

impl TileFillTool {
    fn fill_from(
        &mut self,
        pos: Vec2<i32>,
        ui: &mut TheUI,
        ctx: &mut TheContext,
        project: &mut Project,
        server_ctx: &mut ServerContext,
    ) -> bool {
        let editing_ctx = server_ctx.editing_ctx;

        if matches!(editing_ctx, PixelEditingContext::AvatarFrame(..))
            && server_ctx.avatar_anchor_slot != AvatarAnchorEditSlot::None
        {
            return false;
        }

        let Some(editor) = ui.get_rgba_layout("Tile Editor Dock RGBA Layout") else {
            return false;
        };
        let Some(rgba_view) = editor.rgba_view_mut().as_rgba_view() else {
            return false;
        };

        if rgba_view.has_paste_preview() {
            return false;
        }

        let selection = rgba_view.selection();
        if !selection.is_empty() && !selection.contains(&(pos.x, pos.y)) {
            return false;
        }

        let Some(fill_color) = (if ui.shift {
            Some([0, 0, 0, 0])
        } else {
            editing_ctx.get_draw_color(
                &project.palette,
                server_ctx.palette_opacity,
                server_ctx.body_marker_color,
            )
        }) else {
            return false;
        };

        self.before_tile = None;
        self.before_snapshot = None;
        match editing_ctx {
            PixelEditingContext::Tile(tile_id, _) => {
                if let Some(tile) = project.tiles.get(&tile_id) {
                    self.before_tile = Some(tile.clone());
                }
            }
            _ => {
                if let Some(texture) = project.get_editing_texture(&editing_ctx) {
                    self.before_snapshot = Some((editing_ctx, texture.clone()));
                }
            }
        }

        let Some(texture) = project.get_editing_texture_mut(&editing_ctx) else {
            return false;
        };
        let width = texture.width as i32;
        let height = texture.height as i32;
        if pos.x < 0 || pos.y < 0 || pos.x >= width || pos.y >= height {
            return false;
        }

        let target = texture.get_pixel(pos.x as u32, pos.y as u32);
        if target == fill_color {
            self.before_tile = None;
            self.before_snapshot = None;
            return false;
        }

        let mut stack = vec![(pos.x, pos.y)];
        let mut visited = vec![false; texture.width * texture.height];
        let mut changed = false;

        while let Some((x, y)) = stack.pop() {
            if x < 0 || y < 0 || x >= width || y >= height {
                continue;
            }
            if !selection.is_empty() && !selection.contains(&(x, y)) {
                continue;
            }

            let idx = y as usize * texture.width + x as usize;
            if visited[idx] {
                continue;
            }
            visited[idx] = true;

            if texture.get_pixel(x as u32, y as u32) != target {
                continue;
            }

            texture.set_pixel(x as u32, y as u32, fill_color);
            changed = true;

            stack.push((x + 1, y));
            stack.push((x - 1, y));
            stack.push((x, y + 1));
            stack.push((x, y - 1));
        }

        if !changed {
            self.before_tile = None;
            self.before_snapshot = None;
            return false;
        }

        texture.generate_normals(true);

        match editing_ctx {
            PixelEditingContext::Tile(tile_id, _) => {
                ctx.ui.send(TheEvent::Custom(
                    TheId::named("Tile Updated"),
                    TheValue::Id(tile_id),
                ));
                ctx.ui.send(TheEvent::Custom(
                    TheId::named("Update Tilepicker"),
                    TheValue::Empty,
                ));
            }
            PixelEditingContext::AvatarFrame(..) => {
                ctx.ui.send(TheEvent::Custom(
                    TheId::named("Editing Texture Updated"),
                    TheValue::Empty,
                ));
            }
            PixelEditingContext::None => {}
        }

        true
    }
}