Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Image followup #74

Merged
merged 33 commits into from
Nov 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d1523c0
rename + unnamed member
podusowski Nov 18, 2023
36fb594
rename
podusowski Nov 19, 2023
37b9600
another rename
podusowski Nov 19, 2023
5eef40e
change data type
podusowski Nov 19, 2023
856cd5e
moved angle and scale
podusowski Nov 19, 2023
e45aa68
from_color_image method
podusowski Nov 19, 2023
6d5ef28
reuse code from tiles
podusowski Nov 19, 2023
fadb7b8
cleanup
podusowski Nov 19, 2023
6aa4b2c
reduce visibility
podusowski Nov 19, 2023
d0655fc
inline variable
podusowski Nov 19, 2023
7644970
misc
podusowski Nov 19, 2023
d1d0edc
simplify docstring
podusowski Nov 19, 2023
14edc3a
simplify rect calc
podusowski Nov 19, 2023
dfd9d07
inline variables
podusowski Nov 19, 2023
660e77b
reuse fn
podusowski Nov 19, 2023
0cff7af
simplify by reusing egui's fn
podusowski Nov 19, 2023
63bbb80
rename + unnamed member
podusowski Nov 18, 2023
631ef68
rename
podusowski Nov 19, 2023
3883f18
another rename
podusowski Nov 19, 2023
21f9004
change data type
podusowski Nov 19, 2023
0c3ed56
moved angle and scale
podusowski Nov 19, 2023
55902c4
from_color_image method
podusowski Nov 19, 2023
1a8dfc8
reuse code from tiles
podusowski Nov 19, 2023
5a20c0a
cleanup
podusowski Nov 19, 2023
3329947
reduce visibility
podusowski Nov 19, 2023
9b1c168
inline variable
podusowski Nov 19, 2023
7fb8d5e
misc
podusowski Nov 19, 2023
542ab95
simplify docstring
podusowski Nov 19, 2023
813b588
simplify rect calc
podusowski Nov 19, 2023
b7be8a0
inline variables
podusowski Nov 19, 2023
1bd348a
reuse fn
podusowski Nov 19, 2023
cbae208
simplify by reusing egui's fn
podusowski Nov 19, 2023
3bd1283
Merge branch 'image-followup' of github.com:podusowski/walkers into i…
podusowski Nov 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file.
way of constructing it - `from_lat_lon`, and `from_lon_lat`.
* If center position is detached, zooming using mouse wheel will now keep location under pointer
fixed.
* In `Images` plugin, `scale` and `angle` functions are now part of `Image`.

## 0.12.0

Expand Down
18 changes: 12 additions & 6 deletions demo/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ impl MyApp {
pub fn new(egui_ctx: Context) -> Self {
// Data for the `images` plugin showcase.
let images_plugin_data = ImagesPluginData {
texture: Texture::new(egui_ctx.to_owned(), egui::ColorImage::example()),
texture: Texture::from_color_image(egui::ColorImage::example(), &egui_ctx),
angle: 0.0,
x_scale: 1.0,
y_scale: 1.0,
Expand Down Expand Up @@ -80,9 +80,17 @@ impl eframe::App for MyApp {
style: Style::default(),
},
]))
.with_plugin(Images::new(vec![Image {
position: places::wroclavia(),
texture: self.images_plugin_data.texture.clone(),
.with_plugin(Images::new(vec![{
let mut image = Image::new(
self.images_plugin_data.texture.clone(),
places::wroclavia(),
);
image.scale(
self.images_plugin_data.x_scale,
self.images_plugin_data.y_scale,
);
image.angle(self.images_plugin_data.angle.to_radians());
image
}]))
.with_plugin(CustomShapes {});

Expand Down Expand Up @@ -182,8 +190,6 @@ mod windows {
ui.add(egui::Slider::new(&mut image.angle, 0.0..=360.0).text("Rotate"));
ui.add(egui::Slider::new(&mut image.x_scale, 0.1..=3.0).text("Scale X"));
ui.add(egui::Slider::new(&mut image.y_scale, 0.1..=3.0).text("Scale Y"));
image.texture.scale(image.x_scale, image.y_scale);
image.texture.angle(image.angle.to_radians());
});
});
}
Expand Down
10 changes: 5 additions & 5 deletions walkers/src/download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use futures::StreamExt;
use image::ImageError;
use reqwest::header::USER_AGENT;

use crate::{mercator::TileId, providers::TileSource, tiles::Tile};
use crate::{mercator::TileId, providers::TileSource, tiles::Texture};

#[derive(Debug, thiserror::Error)]
enum Error {
Expand All @@ -19,7 +19,7 @@ async fn download_and_decode(
client: &reqwest::Client,
url: &str,
egui_ctx: &Context,
) -> Result<Tile, Error> {
) -> Result<Texture, Error> {
let image = client
.get(url)
.header(USER_AGENT, "Walkers")
Expand All @@ -36,13 +36,13 @@ async fn download_and_decode(
.await
.map_err(Error::Http)?;

Tile::new(&image, egui_ctx).map_err(Error::Image)
Texture::new(&image, egui_ctx).map_err(Error::Image)
}

async fn download_continuously_impl<S>(
source: S,
mut request_rx: futures::channel::mpsc::Receiver<TileId>,
mut tile_tx: futures::channel::mpsc::Sender<(TileId, Tile)>,
mut tile_tx: futures::channel::mpsc::Sender<(TileId, Texture)>,
egui_ctx: Context,
) -> Result<(), ()>
where
Expand Down Expand Up @@ -73,7 +73,7 @@ where
pub(crate) async fn download_continuously<S>(
source: S,
request_rx: futures::channel::mpsc::Receiver<TileId>,
tile_tx: futures::channel::mpsc::Sender<(TileId, Tile)>,
tile_tx: futures::channel::mpsc::Sender<(TileId, Texture)>,
egui_ctx: Context,
) where
S: TileSource + Send + 'static,
Expand Down
109 changes: 34 additions & 75 deletions walkers/src/extras/images.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,45 @@
use crate::tiles::Texture;
use crate::{Plugin, Position};
use egui::epaint::emath::Rot2;
use egui::{pos2, Color32, ColorImage, Context, Rect, TextureHandle, TextureId};
use egui::{Rect, Vec2};

/// An image to be drawn on the map.
pub struct Image {
/// Geographical position.
pub position: Position,

/// Texture id of image.
scale: Vec2,
angle: Rot2,
pub texture: Texture,
}

impl Image {
pub fn new(texture: Texture, position: Position) -> Self {
Self {
position,
scale: Vec2::splat(1.0),
angle: Rot2::from_angle(0.0),
texture,
}
}

/// Scale the image.
pub fn scale(&mut self, x: f32, y: f32) {
self.scale.x = x;
self.scale.y = y;
}

/// Set the image's angle in radians.
pub fn angle(&mut self, angle: f32) {
self.angle = Rot2::from_angle(angle);
}
}

/// [`Plugin`] which draws given list of images on the map.
pub struct Images {
images: Vec<Image>,
}

#[derive(Clone)]
pub struct Texture {
texture: TextureHandle,
x_scale: f32,
y_scale: f32,
angle: Rot2,
}

impl Images {
pub fn new(images: Vec<Image>) -> Self {
Self { images }
Expand All @@ -33,73 +49,16 @@ impl Images {
impl Plugin for Images {
fn draw(&self, painter: egui::Painter, projector: &crate::Projector) {
for image in &self.images {
let screen_position = projector.project(image.position);
let map_rect = painter.clip_rect();
let texture = &image.texture;

let [w, h] = texture.size();
let w = w as f32 * texture.x_scale;
let h = h as f32 * texture.y_scale;
let mut rect = map_rect.translate(screen_position);

rect.min.x -= w / 2.0;
rect.min.y -= h / 2.0;

rect.max.x = rect.min.x + w;
rect.max.y = rect.min.y + h;

if map_rect.intersects(rect) {
let mut mesh = egui::Mesh::with_texture(texture.id());
let angle = texture.angle;

mesh.add_rect_with_uv(
rect,
Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)),
Color32::WHITE,
);

let origin = egui::Vec2::splat(0.5);
mesh.rotate(angle, rect.min + origin * rect.size());
let rect = Rect::from_center_size(
projector.project(image.position).to_pos2(),
image.texture.size() * image.scale,
);

if painter.clip_rect().intersects(rect) {
let mut mesh = image.texture.mesh_with_rect(rect);
mesh.rotate(image.angle, rect.center());
painter.add(mesh);
}
}
}
}

impl Texture {
/// ⚠️ Make sure to only call this ONCE for each image, i.e. NOT in your main GUI code.
/// The call is NOT immediate safe.
pub fn new(ctx: Context, img: ColorImage) -> Self {
let texture = ctx.load_texture("texture", img, Default::default());

Self {
texture,
x_scale: 1.0,
y_scale: 1.0,
angle: Rot2::from_angle(0.0),
}
}

/// Same as [egui::TextureHandle::id]
/// (https://docs.rs/egui/latest/egui/struct.TextureHandle.html#method.id)
pub fn id(&self) -> TextureId {
self.texture.id()
}

/// Same as [egui::TextureHandle::size] (https://docs.rs/egui/latest/egui/struct.TextureHandle.html#method.size)
pub fn size(&self) -> [usize; 2] {
self.texture.size()
}

/// Scale texture.
pub fn scale(&mut self, x_scale: f32, y_scale: f32) {
self.x_scale = x_scale;
self.y_scale = y_scale;
}

/// Rotate texture.
/// Angle is clockwise in radians. A 𝞃/4 = 90° rotation means rotating the X axis to the Y axis.
pub fn angle(&mut self, angle: f32) {
self.angle = Rot2::from_angle(angle);
}
}
3 changes: 2 additions & 1 deletion walkers/src/extras/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
mod places;
pub use places::{Place, Places, Style};
mod images;
pub use images::{Image, Images, Texture};
pub use crate::tiles::Texture;
pub use images::{Image, Images};
8 changes: 4 additions & 4 deletions walkers/src/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -371,17 +371,17 @@ impl MapMemory {

/// Use simple [flood fill algorithm](https://en.wikipedia.org/wiki/Flood_fill) to draw tiles on the map.
fn flood_fill_tiles(
rect: Rect,
viewport: Rect,
tile_id: TileId,
map_center_projected_position: Pixels,
tiles: &mut Tiles,
meshes: &mut HashMap<TileId, Option<Mesh>>,
) {
let tile_projected = tile_id.project();
let tile_screen_position =
rect.center().to_vec2() + (tile_projected - map_center_projected_position).to_vec2();
viewport.center().to_vec2() + (tile_projected - map_center_projected_position).to_vec2();

if rect.intersects(tiles::rect(tile_screen_position)) {
if viewport.intersects(tiles::rect(tile_screen_position)) {
if let Entry::Vacant(entry) = meshes.entry(tile_id) {
// It's still OK to insert an empty one, as we need to mark the spot for the filling algorithm.
let tile = tiles
Expand All @@ -400,7 +400,7 @@ fn flood_fill_tiles(
.flatten()
{
flood_fill_tiles(
rect,
viewport,
*next_tile_id,
map_center_projected_position,
tiles,
Expand Down
44 changes: 25 additions & 19 deletions walkers/src/tiles.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use std::collections::hash_map::Entry;
use std::collections::HashMap;

use egui::TextureHandle;
use egui::{pos2, Color32, Context, Mesh, Rect, Vec2};
use egui::{ColorImage, TextureHandle};
use image::ImageError;

use crate::download::download_continuously;
Expand All @@ -11,35 +11,41 @@ use crate::mercator::{TileId, TILE_SIZE};
use crate::providers::{Attribution, TileSource};

pub(crate) fn rect(screen_position: Vec2) -> Rect {
Rect::from_min_size(
screen_position.to_pos2(),
Vec2::new(TILE_SIZE as f32, TILE_SIZE as f32),
)
Rect::from_min_size(screen_position.to_pos2(), Vec2::splat(TILE_SIZE as f32))
}

#[derive(Clone)]
pub(crate) struct Tile {
image: TextureHandle,
}
pub struct Texture(TextureHandle);

impl Tile {
impl Texture {
pub fn new(image: &[u8], ctx: &Context) -> Result<Self, ImageError> {
let image = image::load_from_memory(image)?.to_rgba8();
let pixels = image.as_flat_samples();
let image = egui::ColorImage::from_rgba_unmultiplied(
let image = ColorImage::from_rgba_unmultiplied(
[image.width() as _, image.height() as _],
pixels.as_slice(),
);

Ok(Self {
image: ctx.load_texture("tile", image, Default::default()),
})
Ok(Self::from_color_image(image, ctx))
}

/// Load the texture from egui's [`ColorImage`].
pub fn from_color_image(color_image: ColorImage, ctx: &Context) -> Self {
Self(ctx.load_texture("image", color_image, Default::default()))
}

pub(crate) fn size(&self) -> Vec2 {
self.0.size_vec2()
}

pub(crate) fn mesh(&self, screen_position: Vec2) -> Mesh {
self.mesh_with_rect(rect(screen_position))
}

pub fn mesh(&self, screen_position: Vec2) -> Mesh {
let mut mesh = Mesh::with_texture(self.image.id());
pub(crate) fn mesh_with_rect(&self, rect: Rect) -> Mesh {
let mut mesh = Mesh::with_texture(self.0.id());
mesh.add_rect_with_uv(
rect(screen_position),
rect,
Rect::from_min_max(pos2(0., 0.0), pos2(1.0, 1.0)),
Color32::WHITE,
);
Expand All @@ -51,13 +57,13 @@ impl Tile {
pub struct Tiles {
attribution: Attribution,

cache: HashMap<TileId, Option<Tile>>,
cache: HashMap<TileId, Option<Texture>>,

/// Tiles to be downloaded by the IO thread.
request_tx: futures::channel::mpsc::Sender<TileId>,

/// Tiles that got downloaded and should be put in the cache.
tile_rx: futures::channel::mpsc::Receiver<(TileId, Tile)>,
tile_rx: futures::channel::mpsc::Receiver<(TileId, Texture)>,

#[allow(dead_code)] // Significant Drop
runtime: Runtime,
Expand Down Expand Up @@ -92,7 +98,7 @@ impl Tiles {
}

/// Return a tile if already in cache, schedule a download otherwise.
pub(crate) fn at(&mut self, tile_id: TileId) -> Option<Tile> {
pub(crate) fn at(&mut self, tile_id: TileId) -> Option<Texture> {
// Just take one at the time.
match self.tile_rx.try_next() {
Ok(Some((tile_id, tile))) => {
Expand Down