refactor: data, ui
This commit is contained in:
parent
bb016884a3
commit
8d48a56812
8
DRAFT.md
8
DRAFT.md
|
@ -11,3 +11,11 @@
|
|||
- TOML Config file
|
||||
- `~/.config/bobosm/config.toml`
|
||||
|
||||
- LSP
|
||||
- we need to make a MVP for using LSP
|
||||
- We can use LSP to provide auto_completions of tags and values in the editor
|
||||
- custom file extension `.osm.txt` or enabled on demand
|
||||
- we can use helix or nvim as it's compatible with LSP
|
||||
|
||||
- Key sequence feature
|
||||
|
||||
|
|
|
@ -75,10 +75,13 @@ Logout with
|
|||
|
||||
You can upload your changeset,
|
||||
|
||||
Pressing `c` then `u`
|
||||
Pressing `c`
|
||||
|
||||
Your default text editor will appear, you can then preview the changes included in the changeset
|
||||
and type a changeset message.
|
||||
|
||||
The osmChange XML file will be writen in the local state to allow previewing if needed.
|
||||
At the top you can type the tags of the changeset, the `comment` tag is required, `source` is welcomed.
|
||||
At the bottom of this file, a list of new of affected objects are printed with comments starting with `//`
|
||||
|
||||
Then save and quit the editor.
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
// handle the data part of changeset managements
|
||||
|
||||
use fully_pub::fully_pub;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Serialize};
|
||||
|
||||
use quick_xml::se::{to_string_with_root};
|
||||
use xdg::BaseDirectories;
|
||||
|
||||
use crate::{changeset, data::Element, layers::{get_dynamic_data_layer, Layer}, osm_api::OSMApiClient, USER_AGENT};
|
||||
use crate::{data::Element, layers::Layer, USER_AGENT};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[fully_pub]
|
||||
struct Tag {
|
||||
#[serde(rename = "@k")]
|
||||
|
@ -19,6 +19,7 @@ struct Tag {
|
|||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[fully_pub]
|
||||
struct NodeChanges {
|
||||
#[serde(rename = "@id")]
|
||||
id: i64,
|
||||
|
@ -35,6 +36,7 @@ struct NodeChanges {
|
|||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[fully_pub]
|
||||
struct ModifyChanges {
|
||||
#[serde(rename = "node")]
|
||||
nodes: Vec<NodeChanges>
|
||||
|
@ -64,64 +66,8 @@ struct ChangesetMeta {
|
|||
osm: ChangesetMetaInner
|
||||
}
|
||||
|
||||
/*
|
||||
<osm>
|
||||
<changeset>
|
||||
<tag k="created_by" v="JOSM 1.61"/>
|
||||
<tag k="comment" v="Just adding some streetnames"/>
|
||||
...
|
||||
</changeset>
|
||||
...
|
||||
</osm>
|
||||
*/
|
||||
|
||||
|
||||
|
||||
fn build_osm_change_xml(changeset: &ChangesetContent) -> Result<String> {
|
||||
Ok(to_string_with_root("osmChange", changeset)?)
|
||||
}
|
||||
|
||||
// fn open_changeset(&OsmC) -> Result<i64>
|
||||
// {
|
||||
|
||||
// }
|
||||
|
||||
|
||||
/// Will handle all the operations to collect the changes and create a changeset and push it
|
||||
pub fn try_changeset(
|
||||
xdg_dirs: &BaseDirectories,
|
||||
layers: Arc<Mutex<Vec<Layer>>>,
|
||||
osm_client: &OSMApiClient
|
||||
) -> Result<()> {
|
||||
let layers_ref = layers.lock().unwrap();
|
||||
let dynamic_layer = match get_dynamic_data_layer(&layers_ref) {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
return Ok(())
|
||||
}
|
||||
};
|
||||
let changeset_comment = String::from("Add source on POI");
|
||||
let changeset_meta = changeset::ChangesetMeta {
|
||||
osm: changeset::ChangesetMetaInner {
|
||||
tags: vec![
|
||||
Tag {
|
||||
key: "comment".to_string(),
|
||||
value: changeset_comment
|
||||
},
|
||||
Tag {
|
||||
key: "created_by".to_string(),
|
||||
value: USER_AGENT.to_string()
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
println!("changeset_meta: {:?}", changeset_meta);
|
||||
|
||||
// get the changeset id
|
||||
let changeset_id = osm_client.open_changeset(changeset_meta)?;
|
||||
println!("changeset_id: {}", changeset_id);
|
||||
|
||||
// build osmChange
|
||||
/// build struct that will be used as osmChange from the dynamic layers
|
||||
pub fn build_changes_content(changeset_id: i64, dynamic_layer: &Layer) -> ChangesetContent {
|
||||
let mut nodes_changes: Vec<NodeChanges> = vec![];
|
||||
for modified_e_id in &dynamic_layer.modified_data {
|
||||
let modified_e = dynamic_layer.data_source.as_ref().unwrap()
|
||||
|
@ -149,25 +95,15 @@ pub fn try_changeset(
|
|||
_ => continue
|
||||
}
|
||||
}
|
||||
let changeset_content = ChangesetContent {
|
||||
ChangesetContent {
|
||||
version: "0.6".to_string(),
|
||||
generator: USER_AGENT.to_string(),
|
||||
modify: Some(ModifyChanges {
|
||||
nodes: nodes_changes
|
||||
})
|
||||
};
|
||||
println!("changeset_content: {changeset_content:?}");
|
||||
|
||||
//TODO: store XML files in xdg dirs
|
||||
|
||||
println!("CHANGESET: Uploading changeset");
|
||||
osm_client.upload_changes(changeset_id, changeset_content)
|
||||
.context("Uploading changeset content")?;
|
||||
|
||||
println!("CHANGESET: Closing changeset");
|
||||
osm_client.close_changeset(changeset_id)
|
||||
.context("Closing changeset")?;
|
||||
println!("CHANGESET: Changeset closed");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_osm_change_xml(changeset: &ChangesetContent) -> Result<String> {
|
||||
Ok(to_string_with_root("osmChange", changeset)?)
|
||||
}
|
|
@ -1,8 +1,11 @@
|
|||
pub mod changeset;
|
||||
pub mod session;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use fully_pub::fully_pub;
|
||||
|
||||
use raylib::math::Vector2;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[fully_pub]
|
||||
type Tags = Option<HashMap<String, String>>;
|
40
src/data/session.rs
Normal file
40
src/data/session.rs
Normal file
|
@ -0,0 +1,40 @@
|
|||
use std::fs;
|
||||
|
||||
use fully_pub::fully_pub;
|
||||
use anyhow::{Result, Context, anyhow};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use xdg::BaseDirectories;
|
||||
|
||||
// sessions models
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[fully_pub]
|
||||
struct OSMSession {
|
||||
access_token: String,
|
||||
scope: String,
|
||||
token_type: String
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[fully_pub]
|
||||
pub struct Session {
|
||||
osm_session: Option<OSMSession>
|
||||
}
|
||||
|
||||
pub fn read_session(xdg_dirs: &BaseDirectories) -> Result<Session> {
|
||||
// find session file
|
||||
let config_content =
|
||||
fs::read_to_string(xdg_dirs.get_data_file("session.json"))
|
||||
.map_err(|_e| anyhow!("Could not read session JSON file"))?;
|
||||
let session: Session = serde_json::from_str(&config_content)
|
||||
.map_err(|_e| anyhow!("Could not deserialize session file"))?;
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
pub fn write_session(xdg_dirs: &BaseDirectories, session: &Session) -> Result<()> {
|
||||
let session_path = xdg_dirs.place_data_file("session.json").context("Could not create or find session file")?;
|
||||
fs::write(
|
||||
session_path,
|
||||
serde_json::to_string(session).context("Could not serialize session")?
|
||||
).context("Could not write session")?;
|
||||
Ok(())
|
||||
}
|
|
@ -40,33 +40,6 @@ pub fn get_objects_in_area(
|
|||
elements_ids_per_layer
|
||||
}
|
||||
|
||||
pub fn tags_to_text(tags: &Tags) -> String {
|
||||
let tags_v = match tags {
|
||||
None => return "".to_string(),
|
||||
Some(v) => v
|
||||
};
|
||||
let mut out = String::new();
|
||||
for (key, value) in tags_v.iter() {
|
||||
out += &format!("{} = {}\n", &key, &value);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn text_to_tags(tags_as_text: &str) -> Result<Tags> {
|
||||
let mut tags: HashMap<String, String> = HashMap::new();
|
||||
for line in tags_as_text.trim().split('\n') {
|
||||
let line_content = line.to_string();
|
||||
let compos: Vec<&str> = line_content.split('=').collect();
|
||||
if compos.len() != 2 {
|
||||
return Err(anyhow!("Found invalid tags syntax"));
|
||||
}
|
||||
let key = compos.first().unwrap().trim();
|
||||
let val = compos.get(1).unwrap().trim();
|
||||
tags.insert(key.to_string(), val.to_string());
|
||||
}
|
||||
Ok(Some(tags))
|
||||
}
|
||||
|
||||
pub fn edit_tags(
|
||||
xdg_dirs: &BaseDirectories,
|
||||
app_state: Arc<Mutex<AppState>>,
|
||||
|
@ -106,35 +79,44 @@ pub fn edit_tags(
|
|||
/// open external editor
|
||||
pub fn edit_tags_external(xdg_dirs: &BaseDirectories, initial_tags: &Tags) -> Result<Tags>
|
||||
{
|
||||
// write tags file
|
||||
let tags_file_location = xdg_dirs.place_state_file("tags.txt")?;
|
||||
let new_text = open_external_text_editor(
|
||||
xdg_dirs,
|
||||
"tags.txt",
|
||||
&tags_to_text(initial_tags)
|
||||
)?;
|
||||
|
||||
let new_tags = text_to_tags(&new_text)
|
||||
.context("Parsing text tags")?;
|
||||
|
||||
Ok(new_tags)
|
||||
}
|
||||
|
||||
fn open_external_text_editor(xdg_dirs: &BaseDirectories, file_name: &str, source_text: &str) -> Result<String> {
|
||||
// write tmp file
|
||||
let file_path = xdg_dirs.place_state_file(file_name)?;
|
||||
fs::write(
|
||||
&tags_file_location,
|
||||
tags_to_text(initial_tags)
|
||||
).context("Writing tags to file")?;
|
||||
&file_path,
|
||||
source_text
|
||||
).context("Writing temp text to file")?;
|
||||
|
||||
// open cmd
|
||||
// TODO: make the editor configurable
|
||||
let _ = Command::new("alacritty")
|
||||
.arg("--command")
|
||||
.arg("helix")
|
||||
.arg("--config")
|
||||
.arg("/home/mbess/.config/helix/config.toml")
|
||||
.arg(tags_file_location.clone())
|
||||
.arg("~/.config/helix/config.toml")
|
||||
.arg(&file_path)
|
||||
.spawn()
|
||||
.context("Failed to start external editor")?
|
||||
.wait();
|
||||
// wait for editor to exit
|
||||
|
||||
// parse new tags
|
||||
let new_tags_content =
|
||||
fs::read_to_string(&tags_file_location)
|
||||
.context("Reading static osm data file")?;
|
||||
let new_text_content =
|
||||
fs::read_to_string(&file_path)
|
||||
.context("Reading temp text file")?;
|
||||
|
||||
println!("{:?}", new_tags_content);
|
||||
println!("new_text_content: {:?}", new_text_content);
|
||||
|
||||
let new_tags = text_to_tags(&new_tags_content)
|
||||
.context("Parsing text tags")?;
|
||||
|
||||
println!("{:?}", new_tags);
|
||||
|
||||
Ok(new_tags)
|
||||
Ok(new_text_content)
|
||||
}
|
||||
|
|
|
@ -54,8 +54,6 @@ enum LayerKind {
|
|||
DynamicOSMData
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[fully_pub]
|
||||
struct Layer {
|
||||
id: u64,
|
||||
|
@ -109,7 +107,7 @@ pub fn build_displayed_pois(indexed_data: &IndexedData) -> Result<RTree<Displaye
|
|||
let mut importance = -1;
|
||||
// only show some amenities for now
|
||||
if let Some(shop_class) = tags.get("shop") {
|
||||
if ["bakery", "hairdresser"].contains(&shop_class.as_str()) {
|
||||
if ["vacant", "bakery", "hairdresser", "butcher"].contains(&shop_class.as_str()) {
|
||||
importance = 3;
|
||||
}
|
||||
}
|
||||
|
@ -180,7 +178,7 @@ pub fn build_displayed_ways(indexed_data: &IndexedData) -> Result<RTree<Displaye
|
|||
d_ways.push(DisplayedWay {
|
||||
id: way.id,
|
||||
name: final_name,
|
||||
importance: 0,
|
||||
importance: 1,
|
||||
positions: nodes_pos,
|
||||
bbox
|
||||
});
|
||||
|
|
229
src/main.rs
229
src/main.rs
|
@ -1,9 +1,7 @@
|
|||
mod data;
|
||||
mod ui;
|
||||
mod utils;
|
||||
mod changeset;
|
||||
mod layers;
|
||||
mod editor;
|
||||
mod osm_api;
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -11,79 +9,29 @@ mod test_data;
|
|||
#[cfg(test)]
|
||||
mod test_layers;
|
||||
|
||||
use std::{collections::{HashMap, HashSet}, fs, sync::{Arc, Mutex}, thread::{self, JoinHandle}, time::{SystemTime, UNIX_EPOCH}};
|
||||
use std::{collections::{HashMap, HashSet}, fs, sync::{Arc, Mutex}, thread::{self, JoinHandle}};
|
||||
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
|
||||
use data::{bbox_center, IndexedData};
|
||||
use editor::{edit_tags, get_objects_in_area};
|
||||
use data::{bbox_center, session::{read_session, write_session, Session}, IndexedData};
|
||||
use fully_pub::fully_pub;
|
||||
use layers::{load_osm_data, load_osm_zone, load_static_cities_layer, Layer, LayerKind};
|
||||
use osm_api::OSMApiClient;
|
||||
use raylib::prelude::*;
|
||||
use ui::messages::render_messages;
|
||||
use utils::deg2num;
|
||||
use ui::{messages::{render_messages, UiMessage, UiMessageLevel}, selection::get_objects_in_area};
|
||||
use utils::{deg2num, get_envelope_from_bounds};
|
||||
use xdg::BaseDirectories;
|
||||
use std::io::Read;
|
||||
use rstar::AABB;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{Deserialize};
|
||||
use ureq::Agent;
|
||||
use rand::Rng;
|
||||
use crate::utils::CanvasPos;
|
||||
use crate::utils::Invertion;
|
||||
use crate::utils::ToSlice;
|
||||
use crate::utils::ToAABB;
|
||||
|
||||
pub const USER_AGENT: &str = "BobOSM dev";
|
||||
|
||||
trait Invertion {
|
||||
fn invert_for_canvas(&self) -> Vector2;
|
||||
}
|
||||
|
||||
impl Invertion for Vector2 {
|
||||
fn invert_for_canvas(&self) -> Vector2 {
|
||||
Vector2::new(self.x, -self.y)
|
||||
}
|
||||
}
|
||||
|
||||
trait CanvasPos {
|
||||
fn to_canvas_pos(&self, camera: &Camera) -> Vector2;
|
||||
}
|
||||
|
||||
impl CanvasPos for Vector2 {
|
||||
fn to_canvas_pos(&self, camera: &Camera) -> Vector2 {
|
||||
Vector2::new(
|
||||
self.x - camera.bounds.0.x,
|
||||
camera.bounds.1.y - self.y
|
||||
).scale_by(camera.real_to_canvas_factor)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ToSlice {
|
||||
fn to_slice(&self) -> [f32; 2];
|
||||
}
|
||||
|
||||
impl ToSlice for Vector2 {
|
||||
fn to_slice(&self) -> [f32; 2] {
|
||||
[self.x, self.y]
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ToAABB {
|
||||
fn to_aabb(&self) -> AABB<[f32; 2]>;
|
||||
fn to_aabb_with_margin(&self, margin: f32) -> AABB<[f32; 2]>;
|
||||
}
|
||||
|
||||
impl ToAABB for Vector2 {
|
||||
fn to_aabb(&self) -> AABB<[f32; 2]> {
|
||||
AABB::from_point([self.x, self.y])
|
||||
}
|
||||
|
||||
fn to_aabb_with_margin(&self, margin: f32) -> AABB<[f32; 2]> {
|
||||
AABB::from_corners(
|
||||
[self.x - margin, self.y - margin],
|
||||
[self.x + margin, self.y + margin]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Camera {
|
||||
bounds: (Vector2, Vector2),
|
||||
|
@ -101,61 +49,6 @@ enum UiMode {
|
|||
Edit
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[fully_pub]
|
||||
enum UiMessageLevel {
|
||||
Success,
|
||||
Info,
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[fully_pub]
|
||||
struct UiMessage {
|
||||
level: UiMessageLevel,
|
||||
text: String,
|
||||
created_at: u128, // unix epoch in millis
|
||||
ttl: u128 // time to live in millis
|
||||
}
|
||||
|
||||
impl UiMessage {
|
||||
pub fn new(level: UiMessageLevel, text: &str) -> UiMessage {
|
||||
let since_the_epoch = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards");
|
||||
UiMessage {
|
||||
level,
|
||||
text: text.to_string(),
|
||||
created_at: since_the_epoch.as_millis(),
|
||||
ttl: 5000
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_ttl(level: UiMessageLevel, text: String, ttl: u128) -> UiMessage {
|
||||
let mut msg = UiMessage::new(level, &text);
|
||||
msg.ttl = ttl;
|
||||
msg
|
||||
}
|
||||
|
||||
pub fn success(text: &str) -> UiMessage {
|
||||
UiMessage::new(UiMessageLevel::Success, text)
|
||||
}
|
||||
|
||||
pub fn error(text: &str) -> UiMessage {
|
||||
UiMessage::new(UiMessageLevel::Error, text)
|
||||
}
|
||||
|
||||
fn is_dead(&self) -> bool {
|
||||
let current_epoch = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards")
|
||||
.as_millis();
|
||||
|
||||
(current_epoch - self.created_at) > self.ttl
|
||||
}
|
||||
}
|
||||
|
||||
/// store all the app state (UI state)
|
||||
#[derive(Debug)]
|
||||
#[fully_pub]
|
||||
|
@ -183,35 +76,6 @@ struct Config {
|
|||
static_layers: Option<StaticLayers>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
struct OSMSession {
|
||||
access_token: String,
|
||||
scope: String,
|
||||
token_type: String
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Session {
|
||||
osm_session: Option<OSMSession>
|
||||
}
|
||||
|
||||
// #[derive(Debug, Serialize, Deserialize)]
|
||||
// struct Cache {
|
||||
// osm_data: HashMap
|
||||
// }
|
||||
|
||||
|
||||
fn get_envelope_from_bounds(bounds: (Vector2, Vector2), margin_percent: f32) -> AABB<[f32; 2]> {
|
||||
let center = bounds.1.lerp(bounds.0, 0.5);
|
||||
let mut new_bounds = bounds;
|
||||
new_bounds.0 = new_bounds.0 - (center - new_bounds.0).scale_by(margin_percent);
|
||||
new_bounds.1 = new_bounds.1 + (new_bounds.1 - center).scale_by(margin_percent);
|
||||
AABB::from_corners(
|
||||
[new_bounds.0.x, new_bounds.0.y],
|
||||
[new_bounds.1.x, new_bounds.1.y]
|
||||
)
|
||||
}
|
||||
|
||||
fn get_raster_tile(
|
||||
raster_cache: &mut HashMap<(u32, u32, u32), Vec<u8>>,
|
||||
http_agent: &Agent,
|
||||
|
@ -258,16 +122,6 @@ fn get_raster_tile(
|
|||
.map_err(|e| anyhow!("Cannot decode PNG, {:?}", e))
|
||||
}
|
||||
|
||||
// fn get_osm_data(http_agent: &Agent, bounds: (Vector2, Vector2)) -> Result<OSMData> {
|
||||
// /// download local data
|
||||
// // curl -v https://api.openstreetmap.org/api/0.6/map.json -G -d "bbox=1.32495,49.16409,1.34585,49.18485" > map.json
|
||||
|
||||
// let response = http_agent.get("https://api.openstreetmap.org/api/0.6/map.json")
|
||||
// .query("bbox", format!("{},{},{},{}", bounds.0.x, bounds.0.y, bounds.1.x, bounds.1.y))
|
||||
// .call()
|
||||
// .context("Requesting OSM local data")?;
|
||||
// }
|
||||
|
||||
fn read_config(xdg_dirs: &BaseDirectories) -> Result<Config> {
|
||||
let config_content =
|
||||
fs::read_to_string(xdg_dirs.get_config_file("config.toml"))
|
||||
|
@ -277,25 +131,6 @@ fn read_config(xdg_dirs: &BaseDirectories) -> Result<Config> {
|
|||
Ok(config)
|
||||
}
|
||||
|
||||
fn read_session(xdg_dirs: &BaseDirectories) -> Result<Session> {
|
||||
// find session file
|
||||
let config_content =
|
||||
fs::read_to_string(xdg_dirs.get_data_file("session.json"))
|
||||
.map_err(|_e| anyhow!("Could not read session JSON file"))?;
|
||||
let session: Session = serde_json::from_str(&config_content)
|
||||
.map_err(|_e| anyhow!("Could not deserialize session file"))?;
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
fn write_session(xdg_dirs: &BaseDirectories, session: &Session) -> Result<()> {
|
||||
let session_path = xdg_dirs.place_data_file("session.json").context("Could not create or find session file")?;
|
||||
fs::write(
|
||||
session_path,
|
||||
serde_json::to_string(session).context("Could not serialize session")?
|
||||
).context("Could not write session")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn compute_bounds_from_center_zoom(screen_def: (i32, i32), target_center: Vector2, target_zoom: f32) -> (Vector2, Vector2)
|
||||
{
|
||||
let f = 0.00004;
|
||||
|
@ -313,7 +148,9 @@ fn main() -> Result<()> {
|
|||
let (mut rl, thread) = raylib::init()
|
||||
.vsync()
|
||||
.title(USER_AGENT)
|
||||
.resizable() // disable floating window
|
||||
.build();
|
||||
rl.set_exit_key(None);
|
||||
|
||||
let xdg_dirs = xdg::BaseDirectories::with_prefix("bobosm")
|
||||
.context("Locating XDG dirs")?;
|
||||
|
@ -385,22 +222,6 @@ fn main() -> Result<()> {
|
|||
|
||||
let mut move_state = MoveState::Done;
|
||||
|
||||
// first point is left bottom, second point is top right point
|
||||
// let mut current_bounds: (Vector2, Vector2) = ((-4.0, -4.0).into(), (4.0, 4.0).into());
|
||||
// let mut current_bounds: (Vector2, Vector2) = ((-6., 37.0).into(), (12., 55.0).into());
|
||||
|
||||
// additional
|
||||
// let points_repr: Vec<(f32, f32)> = vec![
|
||||
// (0.0, 0.0), (1.0, 1.0), (0.5, 0.5), (-2.0, -2.0), (0.0, -1.0), (1.0, -0.5)
|
||||
// ];
|
||||
// for (i, repr) in points_repr.iter().enumerate() {
|
||||
// tree.insert(LabeledPoint {
|
||||
// importance: 1,
|
||||
// name: format!("{}", i),
|
||||
// pos: Vector2::new(repr.0, repr.1)
|
||||
// });
|
||||
// }
|
||||
|
||||
// load a geojson list of all bigs cities in france
|
||||
// draw the cities points
|
||||
|
||||
|
@ -449,10 +270,7 @@ fn main() -> Result<()> {
|
|||
let mut zoom_factor = 1.0 + -0.2*rl.get_mouse_wheel_move();
|
||||
let mut zoom_focus = real_mouse_pos;
|
||||
|
||||
let is_left_shift_down = rl.is_key_down(KeyboardKey::KEY_LEFT_SHIFT);
|
||||
let is_mouse_left_down = rl.is_mouse_button_down(MouseButton::MOUSE_BUTTON_LEFT);
|
||||
let is_mouse_right_down = rl.is_mouse_button_down(MouseButton::MOUSE_BUTTON_RIGHT);
|
||||
let is_mouse_right_released = rl.is_mouse_button_released(MouseButton::MOUSE_BUTTON_RIGHT);
|
||||
|
||||
// handle INPUT
|
||||
let mut v = Vector2::zero();
|
||||
|
@ -462,6 +280,10 @@ fn main() -> Result<()> {
|
|||
v1 = 0.33;
|
||||
v2 = 3.0;
|
||||
}
|
||||
if rl.is_key_down(KeyboardKey::KEY_LEFT_CONTROL) {
|
||||
v1 = 0.01;
|
||||
v2 = 0.5;
|
||||
}
|
||||
if rl.is_key_down(KeyboardKey::KEY_EQUAL) {
|
||||
zoom_focus = camera.center;
|
||||
zoom_factor -= v2*0.01;
|
||||
|
@ -477,7 +299,11 @@ fn main() -> Result<()> {
|
|||
// changeset mode
|
||||
// for now, generate Osm Change in local data dir
|
||||
println!("try to create and push a changeset");
|
||||
let _ = changeset::try_changeset(&xdg_dirs, layers.clone(), &osm_client);
|
||||
let _ = ui::changeset::try_changeset(
|
||||
&xdg_dirs,
|
||||
layers.clone(),
|
||||
&osm_client
|
||||
);
|
||||
}
|
||||
if rl.is_key_pressed(KeyboardKey::KEY_E) {
|
||||
// toggle edit mode
|
||||
|
@ -488,16 +314,18 @@ fn main() -> Result<()> {
|
|||
}
|
||||
if ui_mode == UiMode::Edit && rl.is_key_pressed(KeyboardKey::KEY_T) {
|
||||
// check if an object is selected
|
||||
let _ = edit_tags(&xdg_dirs, app_state.clone(), layers.clone());
|
||||
let _ = ui::edit_tags(&xdg_dirs, app_state.clone(), layers.clone());
|
||||
}
|
||||
if rl.is_key_pressed(KeyboardKey::KEY_D) {
|
||||
download_zone_asked = true;
|
||||
}
|
||||
if rl.is_key_pressed(KeyboardKey::KEY_UP) || rl.is_key_pressed(KeyboardKey::KEY_K) {
|
||||
// up navigation
|
||||
let bounds_height = (camera.bounds.0.y - camera.bounds.1.y).abs();
|
||||
v = Vector2::new(0.0, v1*bounds_height);
|
||||
}
|
||||
if rl.is_key_pressed(KeyboardKey::KEY_DOWN) || rl.is_key_pressed(KeyboardKey::KEY_J) {
|
||||
// down navigation
|
||||
let bounds_height = (camera.bounds.0.y - camera.bounds.1.y).abs();
|
||||
v = Vector2::new(0.0, -v1*bounds_height);
|
||||
}
|
||||
|
@ -563,7 +391,7 @@ fn main() -> Result<()> {
|
|||
println!("Selected {} new objects", object_count);
|
||||
let mut state = app_state.lock().unwrap();
|
||||
// if no object was selected, then end selection mode
|
||||
if is_left_shift_down {
|
||||
if rl.is_key_down(KeyboardKey::KEY_LEFT_SHIFT) {
|
||||
// when using SHIFT, merge the new selected elements into the existings
|
||||
// for each object in new selection
|
||||
for (i,l) in intersection_objects.iter().enumerate() {
|
||||
|
@ -657,7 +485,12 @@ fn main() -> Result<()> {
|
|||
}
|
||||
let bbox_center_canvas = bbox_center(way.bbox).to_canvas_pos(&camera);
|
||||
// draw name in center of polygon
|
||||
if !way.name.is_empty() {
|
||||
if !way.name.is_empty() && (
|
||||
(camera.zoom <= 0.9 && way.importance >= 10) ||
|
||||
(camera.zoom >= 1.0 && way.importance >= 9) ||
|
||||
camera.zoom >= 1.2
|
||||
)
|
||||
{
|
||||
d.draw_text(
|
||||
&way.name.to_string(),
|
||||
bbox_center_canvas.x.ceil() as i32 - ((way.name.len()/2)*6) as i32,
|
||||
|
@ -722,7 +555,7 @@ fn main() -> Result<()> {
|
|||
{
|
||||
d.draw_text(
|
||||
&point.name.to_string(),
|
||||
canvas_pos.x.ceil() as i32,
|
||||
canvas_pos.x.ceil() as i32 + 8,
|
||||
canvas_pos.y.ceil() as i32,
|
||||
match point.importance {
|
||||
10 => 17,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// all the functions to interact with the OSM API
|
||||
// all the functions to interact with the OSM HTTP API
|
||||
// https://wiki.openstreetmap.org/wiki/API_v0.6
|
||||
|
||||
use std::{time::Duration};
|
||||
|
@ -9,7 +9,7 @@ use quick_xml::se::to_string_with_root;
|
|||
use raylib::math::Vector2;
|
||||
use ureq::{Agent, AgentBuilder};
|
||||
|
||||
use crate::{changeset::{ChangesetContent, ChangesetMeta}, data, OSMSession, Session};
|
||||
use crate::{data::{self, changeset::{ChangesetContent, ChangesetMeta}, session::OSMSession}, Session};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[fully_pub]
|
||||
|
|
92
src/ui/changeset.rs
Normal file
92
src/ui/changeset.rs
Normal file
|
@ -0,0 +1,92 @@
|
|||
use std::{collections::HashMap, sync::{Arc, Mutex}};
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use xdg::BaseDirectories;
|
||||
|
||||
use crate::{data::changeset::{build_changes_content, ChangesetMeta, ChangesetMetaInner, Tag}, layers::{get_dynamic_data_layer, Layer}, osm_api::OSMApiClient, ui::text_editor::{open_external_text_editor, text_to_tags}, USER_AGENT};
|
||||
|
||||
/// Global UI method that manage changeset operation
|
||||
/// Will handle all the operations to collect the changes and create a changeset and push it
|
||||
pub fn try_changeset(
|
||||
xdg_dirs: &BaseDirectories,
|
||||
layers: Arc<Mutex<Vec<Layer>>>,
|
||||
osm_client: &OSMApiClient
|
||||
) -> Result<()> {
|
||||
let layers_ref = layers.lock().unwrap();
|
||||
let dynamic_layer = match get_dynamic_data_layer(&layers_ref) {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
return Ok(())
|
||||
}
|
||||
};
|
||||
let preview_changes_content = build_changes_content(0, dynamic_layer);
|
||||
if (
|
||||
match &preview_changes_content.modify {
|
||||
None => 0,
|
||||
Some(modify) => modify.nodes.len()
|
||||
} == 0
|
||||
) {
|
||||
println!("No changeset to be made, no changes detected");
|
||||
// no changes to be made
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut changeset_text: String = "comment = \nsource = survey".to_string();
|
||||
if let Some(node_changes) = &preview_changes_content.modify {
|
||||
changeset_text.push_str(&format!("\n// {} node(s) changeds\n", &node_changes.nodes.len()));
|
||||
for node in &node_changes.nodes {
|
||||
changeset_text.push_str(&format!("// Node, id: {}, version: {}\n", node.id, node.version));
|
||||
}
|
||||
}
|
||||
|
||||
let new_text = open_external_text_editor(
|
||||
xdg_dirs,
|
||||
"changeset.txt",
|
||||
&changeset_text
|
||||
)?;
|
||||
let user_tags: Vec<Tag> = text_to_tags(&new_text)
|
||||
.context("Parsing changeset text tags")?
|
||||
.unwrap_or(HashMap::new())
|
||||
.iter().map(|(key, value)| {
|
||||
Tag {
|
||||
key: key.to_string(),
|
||||
value: value.to_string()
|
||||
}
|
||||
}).collect();
|
||||
|
||||
let changeset_meta = ChangesetMeta {
|
||||
osm: ChangesetMetaInner {
|
||||
tags: [
|
||||
user_tags,
|
||||
vec![
|
||||
Tag {
|
||||
key: "created_by".to_string(),
|
||||
value: USER_AGENT.to_string()
|
||||
}
|
||||
]
|
||||
].concat()
|
||||
}
|
||||
};
|
||||
println!("changeset_meta: {:?}", changeset_meta);
|
||||
|
||||
// get the changeset id
|
||||
let changeset_id = osm_client.open_changeset(changeset_meta)?;
|
||||
println!("changeset_id: {}", changeset_id);
|
||||
|
||||
let changeset_content = build_changes_content(changeset_id, dynamic_layer);
|
||||
println!("changeset_content: {changeset_content:?}");
|
||||
|
||||
//TODO: store XML files in xdg dirs
|
||||
|
||||
println!("CHANGESET: Uploading changeset");
|
||||
osm_client.upload_changes(changeset_id, changeset_content)
|
||||
.context("Uploading changeset content")?;
|
||||
|
||||
println!("CHANGESET: Closing changeset");
|
||||
osm_client.close_changeset(changeset_id)
|
||||
.context("Closing changeset")?;
|
||||
println!("CHANGESET: Changeset closed");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
4
src/ui/inputs.rs
Normal file
4
src/ui/inputs.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
/// entry point to handle UI inputs
|
||||
fn handle_input() {
|
||||
|
||||
}
|
|
@ -1,7 +1,62 @@
|
|||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use fully_pub::fully_pub;
|
||||
use raylib::{color::Color, drawing::{RaylibDraw, RaylibDrawHandle}, math::Vector2};
|
||||
|
||||
use crate::{UiMessage, UiMessageLevel};
|
||||
#[derive(Debug)]
|
||||
#[fully_pub]
|
||||
enum UiMessageLevel {
|
||||
Success,
|
||||
Info,
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[fully_pub]
|
||||
struct UiMessage {
|
||||
level: UiMessageLevel,
|
||||
text: String,
|
||||
created_at: u128, // unix epoch in millis
|
||||
ttl: u128 // time to live in millis
|
||||
}
|
||||
|
||||
impl UiMessage {
|
||||
pub fn new(level: UiMessageLevel, text: &str) -> UiMessage {
|
||||
let since_the_epoch = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards");
|
||||
UiMessage {
|
||||
level,
|
||||
text: text.to_string(),
|
||||
created_at: since_the_epoch.as_millis(),
|
||||
ttl: 5000
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_ttl(level: UiMessageLevel, text: String, ttl: u128) -> UiMessage {
|
||||
let mut msg = UiMessage::new(level, &text);
|
||||
msg.ttl = ttl;
|
||||
msg
|
||||
}
|
||||
|
||||
pub fn success(text: &str) -> UiMessage {
|
||||
UiMessage::new(UiMessageLevel::Success, text)
|
||||
}
|
||||
|
||||
pub fn error(text: &str) -> UiMessage {
|
||||
UiMessage::new(UiMessageLevel::Error, text)
|
||||
}
|
||||
|
||||
fn is_dead(&self) -> bool {
|
||||
let current_epoch = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards")
|
||||
.as_millis();
|
||||
|
||||
(current_epoch - self.created_at) > self.ttl
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_messages(
|
||||
messages: &Vec<UiMessage>,
|
||||
|
@ -41,5 +96,4 @@ pub fn render_messages(
|
|||
);
|
||||
vertical_offset += 30;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1 +1,67 @@
|
|||
pub mod messages;
|
||||
pub mod changeset;
|
||||
pub mod inputs;
|
||||
pub mod selection;
|
||||
pub mod text_editor;
|
||||
pub mod renderer;
|
||||
|
||||
use std::{collections::HashSet, sync::{Arc, Mutex}};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use text_editor::{open_external_text_editor, tags_to_text, text_to_tags};
|
||||
use xdg::BaseDirectories;
|
||||
|
||||
use crate::{data::Tags, layers::Layer, AppState};
|
||||
|
||||
fn edit_tags_on_element(
|
||||
xdg_dirs: &BaseDirectories,
|
||||
modified_data_layer_ref_mut: &mut HashSet<i64>,
|
||||
id: i64,
|
||||
tags: &mut Tags,
|
||||
) -> Result<()> {
|
||||
let new_text = open_external_text_editor(
|
||||
xdg_dirs,
|
||||
"tags.txt",
|
||||
&tags_to_text(tags)
|
||||
)?;
|
||||
let new_tags = text_to_tags(&new_text)
|
||||
.context("Parsing text tags")?;
|
||||
|
||||
// mark element as modified
|
||||
modified_data_layer_ref_mut.insert(id);
|
||||
*tags = new_tags;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn edit_tags(
|
||||
xdg_dirs: &BaseDirectories,
|
||||
app_state: Arc<Mutex<AppState>>,
|
||||
layers: Arc<Mutex<Vec<Layer>>>
|
||||
) -> Result<()> {
|
||||
let app_state_ref = app_state.lock().unwrap();
|
||||
let mut layers_ref = layers.lock().unwrap();
|
||||
|
||||
// find the first object selected and open editor
|
||||
for (layer_i, layer_selections) in app_state_ref.selected_elements_per_layer.iter().enumerate() {
|
||||
for e_id in layer_selections {
|
||||
let layer_mut_ref = layers_ref.get_mut(layer_i)
|
||||
.ok_or(anyhow!("Could not find layer"))?;
|
||||
let object_element = layer_mut_ref
|
||||
.data_source.as_mut()
|
||||
.ok_or(anyhow!("Could not access layer data source"))?
|
||||
.elements
|
||||
.get_mut(e_id)
|
||||
.ok_or(anyhow!("Could not access selected object source"))?;
|
||||
match object_element {
|
||||
crate::data::Element::Node(node) => {
|
||||
edit_tags_on_element(xdg_dirs, &mut layer_mut_ref.modified_data, node.id, &mut node.tags)?;
|
||||
},
|
||||
crate::data::Element::Way(way) => {
|
||||
edit_tags_on_element(xdg_dirs, &mut layer_mut_ref.modified_data, way.id, &mut way.tags)?;
|
||||
},
|
||||
_ => ()
|
||||
};
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err(anyhow!("No object founds"))
|
||||
}
|
||||
|
|
4
src/ui/renderer.rs
Normal file
4
src/ui/renderer.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
/// takes the pre-processed data to be rendered and turn it into vectors instructions sent to raylib
|
||||
fn map_render() {
|
||||
|
||||
}
|
34
src/ui/selection.rs
Normal file
34
src/ui/selection.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use rstar::AABB;
|
||||
|
||||
use crate::layers::Layer;
|
||||
|
||||
/// return for each layers, the objects id in a given area
|
||||
pub fn get_objects_in_area(
|
||||
layers: Arc<Mutex<Vec<Layer>>>,
|
||||
selection_envelope: AABB<[f32; 2]>
|
||||
) -> Vec<Vec<i64>> {
|
||||
let mut elements_ids_per_layer: Vec<Vec<i64>> = vec![];
|
||||
for layer in layers.lock().unwrap().iter() {
|
||||
let mut elements_ids: Vec<i64> = vec![];
|
||||
if let Some(tree) = &layer.ways_tree {
|
||||
// find a better way to verify if it's intersecting
|
||||
// the better way:
|
||||
// we first get the objects in boundin box + margin
|
||||
// for each way
|
||||
// for each line in way
|
||||
// verify if the point is close to the line
|
||||
for way in tree.locate_in_envelope_intersecting(&selection_envelope) {
|
||||
// elements_ids.push(way.id);
|
||||
}
|
||||
}
|
||||
if let Some(tree) = &layer.pois_tree {
|
||||
for point in tree.locate_in_envelope(&selection_envelope) {
|
||||
elements_ids.push(point.id);
|
||||
}
|
||||
}
|
||||
elements_ids_per_layer.push(elements_ids);
|
||||
}
|
||||
elements_ids_per_layer
|
||||
}
|
69
src/ui/text_editor.rs
Normal file
69
src/ui/text_editor.rs
Normal file
|
@ -0,0 +1,69 @@
|
|||
use std::{collections::HashMap, fs, process::Command};
|
||||
|
||||
use crate::data::Tags;
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use xdg::BaseDirectories;
|
||||
|
||||
pub fn tags_to_text(tags: &Tags) -> String {
|
||||
let tags_v = match tags {
|
||||
None => return "".to_string(),
|
||||
Some(v) => v
|
||||
};
|
||||
let mut out = String::new();
|
||||
for (key, value) in tags_v.iter() {
|
||||
out += &format!("{} = {}\n", &key, &value);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn text_to_tags(tags_as_text: &str) -> Result<Tags> {
|
||||
let mut tags: HashMap<String, String> = HashMap::new();
|
||||
for line in tags_as_text.trim().split('\n') {
|
||||
let line_content = line.to_string();
|
||||
if line_content.is_empty() || line_content.starts_with("//") {
|
||||
continue;
|
||||
}
|
||||
let inline_comment_compos: Vec<&str> = line_content.split(" //").collect();
|
||||
let line_true_content = inline_comment_compos
|
||||
.iter().next()
|
||||
.expect("Expected at least one component");
|
||||
let compos: Vec<&str> = line_true_content.split('=').collect();
|
||||
if compos.len() != 2 {
|
||||
return Err(anyhow!("Found invalid tags syntax"));
|
||||
}
|
||||
let key = compos.first().unwrap().trim();
|
||||
let val = compos.get(1).unwrap().trim();
|
||||
tags.insert(key.to_string(), val.to_string());
|
||||
}
|
||||
Ok(Some(tags))
|
||||
}
|
||||
|
||||
pub fn open_external_text_editor(xdg_dirs: &BaseDirectories, file_name: &str, source_text: &str) -> Result<String> {
|
||||
// write tmp file
|
||||
let file_path = xdg_dirs.place_state_file(file_name)?;
|
||||
fs::write(
|
||||
&file_path,
|
||||
source_text
|
||||
).context("Writing temp text to file")?;
|
||||
|
||||
// open cmd
|
||||
// TODO: make the editor configurable
|
||||
let _ = Command::new("alacritty")
|
||||
.arg("--command")
|
||||
.arg("helix")
|
||||
.arg("--config")
|
||||
.arg("/home/mbess/.config/helix/config.toml")
|
||||
.arg(&file_path)
|
||||
.spawn()
|
||||
.context("Failed to start external editor")?
|
||||
.wait();
|
||||
// wait for editor to exit
|
||||
|
||||
let new_text_content =
|
||||
fs::read_to_string(&file_path)
|
||||
.context("Reading temp text file")?;
|
||||
|
||||
// println!("new_text_content: {:?}", new_text_content);
|
||||
|
||||
Ok(new_text_content)
|
||||
}
|
68
src/utils.rs
68
src/utils.rs
|
@ -1,4 +1,7 @@
|
|||
use raylib::consts;
|
||||
use raylib::{consts, math::Vector2};
|
||||
use rstar::AABB;
|
||||
|
||||
use crate::Camera;
|
||||
|
||||
pub fn deg2num(lat_deg: f64, lon_deg: f64, zoom: u32) -> (u32, u32) {
|
||||
// Web mercator projection
|
||||
|
@ -9,3 +12,66 @@ pub fn deg2num(lat_deg: f64, lon_deg: f64, zoom: u32) -> (u32, u32) {
|
|||
|
||||
(xtile, ytile)
|
||||
}
|
||||
|
||||
pub fn get_envelope_from_bounds(bounds: (Vector2, Vector2), margin_percent: f32) -> AABB<[f32; 2]> {
|
||||
let center = bounds.1.lerp(bounds.0, 0.5);
|
||||
let mut new_bounds = bounds;
|
||||
new_bounds.0 = new_bounds.0 - (center - new_bounds.0).scale_by(margin_percent);
|
||||
new_bounds.1 = new_bounds.1 + (new_bounds.1 - center).scale_by(margin_percent);
|
||||
AABB::from_corners(
|
||||
[new_bounds.0.x, new_bounds.0.y],
|
||||
[new_bounds.1.x, new_bounds.1.y]
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
pub trait Invertion {
|
||||
fn invert_for_canvas(&self) -> Vector2;
|
||||
}
|
||||
|
||||
impl Invertion for Vector2 {
|
||||
fn invert_for_canvas(&self) -> Vector2 {
|
||||
Vector2::new(self.x, -self.y)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CanvasPos {
|
||||
fn to_canvas_pos(&self, camera: &Camera) -> Vector2;
|
||||
}
|
||||
|
||||
impl CanvasPos for Vector2 {
|
||||
fn to_canvas_pos(&self, camera: &Camera) -> Vector2 {
|
||||
Vector2::new(
|
||||
self.x - camera.bounds.0.x,
|
||||
camera.bounds.1.y - self.y
|
||||
).scale_by(camera.real_to_canvas_factor)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ToSlice {
|
||||
fn to_slice(&self) -> [f32; 2];
|
||||
}
|
||||
|
||||
impl ToSlice for Vector2 {
|
||||
fn to_slice(&self) -> [f32; 2] {
|
||||
[self.x, self.y]
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ToAABB {
|
||||
fn to_aabb(&self) -> AABB<[f32; 2]>;
|
||||
fn to_aabb_with_margin(&self, margin: f32) -> AABB<[f32; 2]>;
|
||||
}
|
||||
|
||||
impl ToAABB for Vector2 {
|
||||
fn to_aabb(&self) -> AABB<[f32; 2]> {
|
||||
AABB::from_point([self.x, self.y])
|
||||
}
|
||||
|
||||
fn to_aabb_with_margin(&self, margin: f32) -> AABB<[f32; 2]> {
|
||||
AABB::from_corners(
|
||||
[self.x - margin, self.y - margin],
|
||||
[self.x + margin, self.y + margin]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue