document more code, fix some code smells

This commit is contained in:
Tove 2023-12-20 22:20:05 +01:00
parent 467f62d381
commit d309181281
Signed by: TudbuT
GPG key ID: B3CF345217F202D3
11 changed files with 133 additions and 66 deletions

View file

@ -21,6 +21,8 @@ use DrawColor::*;
impl DrawColor {
pub fn menu(app: &mut App, ui: &mut Ui) {
let mut clicked = false;
// rust does not have reflection at the moment so this looks a bit ugly and repetitive
// adds button to the menu
clicked |= ui.radio_value(&mut app.color, Black, "Black").clicked();
clicked |= ui.radio_value(&mut app.color, White, "White").clicked();
clicked |= ui.radio_value(&mut app.color, Red, "Red").clicked();
@ -31,6 +33,7 @@ impl DrawColor {
clicked |= ui.radio_value(&mut app.color, Brown, "Brown").clicked();
clicked |= ui.radio_value(&mut app.color, Aqua, "Aqua").clicked();
clicked |= ui.radio_value(&mut app.color, Purple, "Purple").clicked();
// if any have been clicked, change the color to it
if clicked {
app.draw.px = app.color.into_color();
}

View file

@ -36,7 +36,7 @@ impl ChangeRect {
pub fn push(&mut self, x: usize, y: usize) {
self.count += 1;
if self.changelist.is_some() && self.count >= self.max_changelist_len {
self.changelist = None;
self.changelist = None; // changelist has "overflown" (too much to update single points)
}
if let Some(ref mut changelist) = self.changelist {
changelist.push([x, y]);
@ -47,6 +47,8 @@ impl ChangeRect {
self.empty = false;
return;
}
// expand area to include this point
if x < self.min[0] {
self.min[0] = x;
}
@ -62,12 +64,14 @@ impl ChangeRect {
}
pub fn all(&mut self, rect: Rect) {
// only pushes the corners as an optimization
self.push(rect.min.x as usize, rect.min.y as usize);
self.push(rect.max.x as usize, rect.max.y as usize);
self.changelist = None;
self.count += rect.area() as usize - 2;
self.changelist = None; // force "overflown" changelist
self.count += rect.area() as usize - 2; // add other pixels in rectangle that werent added by push
}
/// "takes" the changes, resetting this struct and returning the area/pixels to update
pub fn take(&mut self) -> ChangedRect {
self.empty = true;
let count = self.count;
@ -103,7 +107,7 @@ impl<T: Copy + Sized> FlatArea<T> for Array<T, 2> {
// SAFETY: [all unsafe operations explained in further comments]
unsafe {
let mut r_ptr = r.as_mut_ptr();
// SAFETY: Every element will be overwritten
// SAFETY: Every element will be overwritten => no garbage data will be left
r.set_len(size[0] * size[1]);
for i in 0..size[1] {
let idx = start[0] + (start[1] + i) * y_len;
@ -114,7 +118,7 @@ impl<T: Copy + Sized> FlatArea<T> for Array<T, 2> {
self_flat[idx..]
.as_ptr()
.copy_to_nonoverlapping(r_ptr, size[0]);
r_ptr = r_ptr.offset(size[0] as isize);
r_ptr = r_ptr.add(size[0]);
}
}
r

View file

@ -16,6 +16,7 @@ impl Default for Debug {
}
}
// bad (but good-enough for this purpose) random number generator
fn rand(rand: &mut u64) -> u8 {
let state = *rand;
let r = ((state & 0b1111) << 60) + ((state >> 60) & 0b1111);

View file

@ -8,6 +8,7 @@ pub enum DialogAction {
}
impl App {
// handles Open
pub fn open_file(&mut self) {
let mut dialog = egui_file::FileDialog::open_file(None);
dialog.open();
@ -15,6 +16,7 @@ impl App {
self.dialog = Some(dialog);
}
// handles Save and SaveAs
pub fn save_file(&mut self, ask_name: bool) {
if ask_name || self.filename.is_none() {
let mut dialog = egui_file::FileDialog::save_file(None);
@ -26,21 +28,16 @@ impl App {
}
}
// called on app update to update dialogs too
pub fn handle_dialogs(&mut self, ctx: &Context) {
if let Some(ref mut d) = self.dialog {
if d.show(ctx).selected() {
if let Some(file) = d.path() {
if let Some(ref mut dialog) = self.dialog {
if dialog.show(ctx).selected() {
if let Some(file) = dialog.path() {
self.filename = Some(file.to_str().expect("invalid file name").to_owned());
// do IO operations associated with dialog
match self.dialog_action.as_ref().unwrap() {
DialogAction::Open => {
self.filename =
Some(file.to_str().expect("invalid file name").to_owned());
self.load();
}
DialogAction::Save => {
self.filename =
Some(file.to_str().expect("invalid file name").to_owned());
self.save();
}
DialogAction::Open => self.load(),
DialogAction::Save => self.save(),
};
self.dialog_action = None;
self.dialog = None;

View file

@ -1,3 +1,5 @@
//! These are quite complex, but I will do my best to explain them anyway.
use std::f32::consts::PI;
use egui::Color32;
@ -87,12 +89,14 @@ impl App {
self.image[[x, y]] = Color32::from_rgb((px >> 16) as u8, (px >> 8) as u8, px as u8);
self.changes.push(x, y);
}
/// Does not ignore pixels out of bounds, panic!s instead.
pub fn set_px_unchecked(&mut self, x: usize, y: usize, col: Color32) {
self.image[[x, y]] = col;
self.changes.push(x, y);
}
/// Draws a dot of arbitrary size (size 1 has no corners, all others do)
/// Draws a dot of arbitrary size. This is one px for a size of 0, a plus for size 1, and a rectangle of side length (size - 1) * 2
pub fn draw_dot(&mut self, draw: DrawParams) {
self.set_px(draw.offset(0, 0));
@ -131,27 +135,32 @@ impl App {
size: size2,
..
} = draw2;
let dx = x2 as f32 - x1 as f32;
let dy = y2 as f32 - y1 as f32;
let dsize = size2 as f32 - size1 as f32;
let dist = (dx * dx + dy * dy).sqrt();
let step_x = dx / dist;
let step_y = dy / dist;
let step_size = dsize / dist;
let dx = x2 as f32 - x1 as f32; // the offset in x direction
let dy = y2 as f32 - y1 as f32; // the offset in y direction
let dsize = size2 as f32 - size1 as f32; // the change in size over the distance
let dist = (dx * dx + dy * dy).sqrt(); // the distance
let step_x = dx / dist; // the change of x over a distance of 1 pixel
let step_y = dy / dist; // the change of y over a distance of 1 pixel
let step_size = dsize / dist; // the change in size over a distance of 1 pixel
// the values as floats
let mut fx = x1 as f32;
let mut fy = y1 as f32;
let mut fsize = size1 as f32;
// loop until distance is reached, but overshoot
for _ in 0..(dist + 1.0) as usize {
// draw
func(
self,
draw1.at_sized(fx as usize, fy as usize, fsize.round() as usize),
);
if fx as usize == x2 && fy as usize == y2 {
break;
}
// modify values by the needed amount
fx += step_x;
fy += step_y;
fsize += step_size;
// if arrived at destination, stop (this is why the overshoot is not a problem)
if fx as usize == x2 && fy as usize == y2 {
break;
}
}
}
@ -161,7 +170,8 @@ impl App {
self.last_mouse_pos = Some(draw);
}
/// draws a polygon (or circle) by drawing around a center point at angles in increments of π*2 / n
/// Draws an n-gon (polygon) with an arbitrary rotation, radius, and amount of corners (n)
/// by drawing around a center point at angles in increments of π*2 / n
pub fn draw_ngon(
&mut self,
draw: DrawParams,
@ -170,26 +180,40 @@ impl App {
mut radius_y: f32,
begin_angle: f32,
) {
// n = 0 => draw a circle
if n == 0 {
n = (radius_x.abs().max(radius_y.abs()) * PI) as usize;
}
let begin_angle = (begin_angle - 90.0) / 180.0 * PI;
if n == 4 {
radius_x /= begin_angle.cos();
radius_y /= begin_angle.sin();
n = (radius_x.abs().max(radius_y.abs()) * PI * 2.0) as usize; // circle can be approximated by having as many corners as pixels
}
// convert rotation to radians
let begin_angle = begin_angle / 180.0 * PI /*start at top:*/ + PI;
// to make pulling the usual shapes feel more natural
if n == 3 {
radius_x /= (begin_angle + 2.0 / 3.0 * PI).cos();
radius_x /= (begin_angle + 2.0 / 3.0 * PI).sin();
}
if n == 4 {
radius_x /= begin_angle.sin();
radius_y /= begin_angle.cos();
}
// amount of radians between each corner if it were on a circle
let angle_increment = PI * 2.0 / n as f32;
// start one corner after the starting point because we draw lines
let mut current_angle = angle_increment + begin_angle;
// center
let fx = draw.loc.x as f32;
let fy = draw.loc.y as f32;
let mut last_x = begin_angle.cos() * radius_x;
let mut last_y = begin_angle.sin() * radius_y;
let mut last_x = begin_angle.sin() * radius_x;
let mut last_y = begin_angle.cos() * radius_y;
// loop over corners and draw a line from the last to the current
for _ in 0..n {
let new_x = current_angle.cos() * radius_x;
let new_y = current_angle.sin() * radius_y;
let new_x = current_angle.sin() * radius_x;
let new_y = current_angle.cos() * radius_y;
self.draw_line(
draw.at((fx + last_x) as usize, (fy + last_y) as usize),
draw.at((fx + new_x) as usize, (fy + new_y) as usize),

View file

@ -7,6 +7,7 @@ use crate::{
App,
};
/// The state struct for the fill algorithm
struct Filler<'app> {
col: Color32,
open: Vec<Location>,
@ -18,9 +19,10 @@ struct Filler<'app> {
impl<'app> Filler<'app> {
fn new(app: &'app mut App, draw: DrawParams) -> Self {
Self {
col: app.image[[draw.loc.x, draw.loc.y]],
col: app.image[[draw.loc.x, draw.loc.y]], // color to replace
open: vec![draw.loc],
closed: HashSet::with_capacity(128),
// color to fill with
fill_color: Color32::from_rgb(
(draw.px >> 16) as u8,
(draw.px >> 8) as u8,
@ -31,32 +33,41 @@ impl<'app> Filler<'app> {
}
fn push(&mut self, node: Location) {
// has this been visited yet?
if !self.closed.contains(&node) {
self.open.push(node);
}
}
// Fills the area.
// 1. add current pixel to open list
// 2. pop first pixel from open list
// 3. add it to closed list
// 4. check if it is valid
// 5. color it
// 6. add its neighbors to the open list
// 7. repeat 2 until open list is empty
/// Fills the area.
///
/// "nodes" are pixels
/// 1. add current pixel to open list
/// 2. pop first pixel from open list
/// 3. add it to closed list
/// 4. check if it is valid
/// 5. color it
/// 6. add its neighbors to the open list
/// 7. repeat from 2 on until open list is empty
fn fill(&mut self) {
let img_size = self.app.image.size();
// until all nodes are visited
while let Some(node) = self.open.pop() {
// mark node done
self.closed.insert(node);
// if node is invalid, skip it
if node.x >= img_size[0] || node.y >= img_size[1] {
continue;
}
// if node is wrong color, skip it
if self.app.image[[node.x, node.y]] != self.col {
continue;
}
// set color without bounds check (already done above)
self.app.set_px_unchecked(node.x, node.y, self.fill_color);
// mark neighbors to be visited
self.push(node.offset(-1, 0));
self.push(node.offset(1, 0));
self.push(node.offset(0, -1));

View file

@ -6,6 +6,7 @@ use crate::App;
impl App {
/// SAFETY: Call only when self.filename is present
/// loads a file from disk (called after open dialog is confirmed)
pub fn load(&mut self) {
if let Ok(x) = ImageReader::open(self.filename.as_ref().unwrap())
.expect("This file can't be opened due to an IO error")
@ -30,6 +31,7 @@ impl App {
}
/// SAFETY: Call only when self.filename is present
/// saves the image to disk
pub fn save(&mut self) {
let size = self.image.size();
DynamicImage::ImageRgb8(ImageBuffer::from_fn(

View file

@ -84,8 +84,12 @@ impl App {
impl eframe::App for App {
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
ctx.request_repaint_after(Duration::from_millis(0));
// try to do 90fps
ctx.request_repaint_after(Duration::from_millis(1000 / 90));
self.handle_dialogs(ctx);
// the content frame
let f = Frame::none()
.inner_margin(Margin::same(2.0))
.fill(Color32::from_rgb(0x00, 0x00, 0x00));
@ -153,20 +157,27 @@ impl eframe::App for App {
[size.x as usize, size.y as usize],
);
self.image_to_texture(&mut ctx.tex_manager().write());
// draw the texture
let r = ui.add(Image::from_texture(SizedTexture::new(self.tex, size)));
// handle mouse and keyboard input
// handle keyboard and mouse input
ui.input(|inp| {
// get pointer pos offset to be in the image or return if its not inside the window
let Some(pointer_pos) = r.hover_pos().map(|pos| pos - r.rect.min) else {
return;
};
// return if not actually on the image
if !r.hovered() {
return; // we don't need to handle it if it's not in focus
}
let Some(pointer_pos) = r.hover_pos().map(|x| x - r.rect.min) else {
return; // we don't need to handle it if the image is not under the cursor
};
// handle pulling shapes
if inp.pointer.secondary_down() || self.pull_start.is_some() {
self.pull(&inp, [pointer_pos.x as usize, pointer_pos.y as usize]);
self.pull(inp, [pointer_pos.x as usize, pointer_pos.y as usize]);
return;
}
if inp.key_down(Key::D) {
self.draw_ngon(
self.draw.at(pointer_pos.x as usize, pointer_pos.y as usize),
@ -198,8 +209,10 @@ impl eframe::App for App {
if inp.pointer.primary_down() {
let draw = self.draw.at(pointer_pos.x as usize, pointer_pos.y as usize);
if self.mode.run_once() {
// don't interpolate
self.mode.into_fn()(self, draw);
} else {
// interpolate
self.draw_mouse(draw, self.mode.into_fn());
}
} else {

View file

@ -22,7 +22,9 @@ impl Mode {
ui.radio_value(&mut app.mode, Fill, "Fill");
}
/// Some things shouldn't be interpolated and only run once
pub fn run_once(self) -> bool {
#[allow(clippy::match_like_matches_macro)] // this may have more added later
match self {
Fill => true,
_ => false,
@ -32,7 +34,7 @@ impl Mode {
/// universalizes the mode into a single function with pre-set size
pub fn into_fn_sized(self, radius_x: f32, radius_y: f32) -> fn(&mut App, DrawParams) {
static mut RADIUS: (f32, f32) = (0.0, 0.0);
// this is single-threaded
// this is single-threaded so a static is fine
unsafe {
RADIUS = (radius_x, radius_y); // need this for compatible match arms
match self {

View file

@ -7,6 +7,7 @@ impl App {
self.real_image = self.image.clone();
}
// not perfectly efficient, but fast enough to be responsive
pub fn pull(&mut self, inp: &InputState, pointer_pos: [usize; 2]) {
if let Some(pull_start) = self.pull_start {
// clone the image to reset it, then draw the current state of the pulled brush
@ -19,10 +20,10 @@ impl App {
));
// the distance pulled divided by two (-> the radius)
let pull_x = (pull_start[0] as isize - pointer_pos[0] as isize) / 2;
let pull_y = (pull_start[1] as isize - pointer_pos[1] as isize) / 2;
let pull_x = (pointer_pos[0] as isize - pull_start[0] as isize) / 2;
let pull_y = (pointer_pos[1] as isize - pull_start[1] as isize) / 2;
let pull_size = if inp.modifiers.shift {
// if shift is pressed, both sizes are the same
// if shift is pressed, both sizes are the same (specifically, the biggest of the two)
#[inline]
fn sign(x: isize) -> isize {
if x < 0 {
@ -37,23 +38,25 @@ impl App {
} else {
[pull_x, pull_y]
};
// draw:
self.mode
.into_fn_sized(pull_size[0] as f32, pull_size[1] as f32)(
self,
self.draw.at(
// this is going to be the center
(pull_start[0] as isize - pull_size[0]) as usize,
(pull_start[1] as isize - pull_size[1]) as usize,
(pull_start[0] as isize + pull_size[0]) as usize,
(pull_start[1] as isize + pull_size[1]) as usize,
),
);
// resets the pull
// reset and save the pull if user has stopped pulling
if !inp.pointer.secondary_down() {
self.pull_start = None;
self.sync();
}
} else {
// starts a pull
// start a pull
self.pull_start = Some(pointer_pos);
self.sync();
}

View file

@ -7,12 +7,15 @@ use micro_ndarray::Array;
use crate::{compress::FlatArea, App};
impl App {
/// changes the image size when resizing window
pub fn correct_tex_size(&mut self, texman: &mut TextureManager, window_size: [usize; 2]) {
if self.image.size() == window_size {
return;
}
// creates a new image and transfers the pixels
let mut new_image = Array::new_with([window_size[0], window_size[1]], Color32::WHITE);
for (pos, pixel) in self.image.iter() {
// if its within the image, set it
if let Some(px) = new_image.get_mut(pos) {
*px = *pixel;
}
@ -25,11 +28,12 @@ impl App {
Pos2::new(window_size[0] as f32, window_size[1] as f32),
));
// create a renderable texture from the new image
let cimg = ColorImage {
size: self.image.size(),
pixels: self.image.as_flattened().to_vec(),
};
texman.free(self.tex);
texman.free(self.tex); // drop old texture
self.tex = texman.alloc(
"canvas".to_owned(),
ImageData::Color(Arc::new(cimg)),
@ -38,11 +42,13 @@ impl App {
minification: TextureFilter::Linear,
},
);
// write image into the texture
self.image_to_texture(texman);
}
pub fn image_to_texture(&mut self, texman: &mut TextureManager) {
let changes = self.changes.take();
// if its so few it can be updated in single pixels
if let Some(changelist) = changes.changelist {
for change in changelist {
let cimg = ColorImage {
@ -63,10 +69,11 @@ impl App {
}
return;
}
// if its only a region
if changes.area < self.image.as_flattened().len() / 2 {
let cimg = ColorImage {
size: changes.size,
pixels: self.image.area_flat(changes.min, changes.size),
pixels: self.image.area_flat(changes.min, changes.size), // only an area of the image, as a flattened array of pixels
};
texman.set(
self.tex,