diff --git a/Cargo.lock b/Cargo.lock index e6642ec..2149d7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,6 +92,37 @@ dependencies = [ "thiserror", ] +[[package]] +name = "async-channel" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "once_cell", + "slab", +] + +[[package]] +name = "async-task" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30696a84d817107fc028e049980e09d5e140e8da8f1caeb17e8e950658a3cea9" + [[package]] name = "atomic_refcell" version = "0.1.8" @@ -115,6 +146,157 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "bevy_app" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32660ae99fa3498ca379de28b7e2f447e6531b0e432bf200901efeec075553c1" +dependencies = [ + "bevy_derive", + "bevy_ecs", + "bevy_reflect", + "bevy_utils", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "bevy_core" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12c0f8614b6014671ab60bacb8bf681373d08b0bb15633b8ef72b895cf966d29" +dependencies = [ + "bevy_app", + "bevy_derive", + "bevy_ecs", + "bevy_math", + "bevy_reflect", + "bevy_tasks", + "bevy_utils", + "bytemuck", +] + +[[package]] +name = "bevy_derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6abddf2ed415f31d28a9bf9ab3c0bc857e98a722858d38dba65bdda481f8d714" +dependencies = [ + "bevy_macro_utils", + "quote", + "syn", +] + +[[package]] +name = "bevy_ecs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79e67dd06b14e787d2026fe6e2b63f67482afcc62284f20ea2784d8b0662e95f" +dependencies = [ + "async-channel", + "bevy_ecs_macros", + "bevy_reflect", + "bevy_tasks", + "bevy_utils", + "downcast-rs", + "fixedbitset", + "fxhash", + "serde", + "thiserror", +] + +[[package]] +name = "bevy_ecs_macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "718923a491490bd81074492d61fc08134f9c62a29ba8666818cd7a6630421246" +dependencies = [ + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bevy_macro_utils" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ddfc33a99547e36718e56e414541e461c74ec318ff987a1e9f4ff46d0dacbb" +dependencies = [ + "cargo-manifest", + "quote", + "syn", +] + +[[package]] +name = "bevy_math" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20288df0f70ff258bbaffaf55209f1271a7436438591bbffc3d81e4d84b423f2" +dependencies = [ + "bevy_reflect", + "glam", +] + +[[package]] +name = "bevy_reflect" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d0793107bc4b7c6bd04232d739fc8d70aa5fb313bfad6e850f91f79b2557eed" +dependencies = [ + "bevy_reflect_derive", + "bevy_utils", + "downcast-rs", + "erased-serde", + "glam", + "parking_lot 0.11.2", + "serde", + "smallvec", + "thiserror", +] + +[[package]] +name = "bevy_reflect_derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81c88de8067d19dfde31662ee78e3ee6971e2df27715799f91b515b37a636677" +dependencies = [ + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn", + "uuid", +] + +[[package]] +name = "bevy_tasks" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2b0f0b86c8f78c53a2d4c669522f45e725ed9d9c3d734f54ec30876494e04e" +dependencies = [ + "async-channel", + "async-executor", + "event-listener", + "futures-lite", + "num_cpus", + "wasm-bindgen-futures", +] + +[[package]] +name = "bevy_utils" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f354c584812996febd48cc885f36b23004b49d6680e73fc95a69a2bb17a48e5" +dependencies = [ + "ahash", + "bevy_derive", + "getrandom", + "hashbrown", + "instant", + "tracing", + "uuid", +] + [[package]] name = "bincode" version = "1.3.3" @@ -174,12 +356,24 @@ dependencies = [ "syn", ] +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + [[package]] name = "bytes" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +[[package]] +name = "cache-padded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" + [[package]] name = "calloop" version = "0.9.3" @@ -190,6 +384,17 @@ dependencies = [ "nix", ] +[[package]] +name = "cargo-manifest" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af6d65c7592744998c67947ec771c62687c76f00179a83ffd563c0482046bb98" +dependencies = [ + "serde", + "serde_derive", + "toml", +] + [[package]] name = "cast" version = "0.2.7" @@ -295,6 +500,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" +dependencies = [ + "cache-padded", +] + [[package]] name = "core-foundation" version = "0.7.0" @@ -655,6 +869,15 @@ dependencies = [ "parking_lot 0.12.0", ] +[[package]] +name = "erased-serde" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad132dd8d0d0b546348d7d86cb3191aad14b34e5f979781fc005c80d4ac67ffd" +dependencies = [ + "serde", +] + [[package]] name = "error-code" version = "2.3.1" @@ -665,6 +888,27 @@ dependencies = [ "str-buf", ] +[[package]] +name = "event-listener" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" + +[[package]] +name = "fastrand" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + +[[package]] +name = "fixedbitset" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279fb028e20b3c4c320317955b77c5e0c9701f05a1d309905d6fc702cdc5053e" + [[package]] name = "fnv" version = "1.0.7" @@ -696,6 +940,42 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures-core" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" + +[[package]] +name = "futures-io" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" + +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "gethostname" version = "0.2.3" @@ -730,6 +1010,16 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "glam" +version = "0.20.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43e957e744be03f5801a55472f593d43fabdebf25a4585db250f04d86b1675f" +dependencies = [ + "bytemuck", + "serde", +] + [[package]] name = "glow" version = "0.11.2" @@ -825,6 +1115,10 @@ name = "hashbrown" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", + "serde", +] [[package]] name = "hermit-abi" @@ -895,6 +1189,18 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" +[[package]] +name = "iyes_loopless" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e37cbe1fdc9aeed2d5ec064d3306ba4318723c20c5b0e4662d03c49fedcd491b" +dependencies = [ + "bevy_app", + "bevy_core", + "bevy_ecs", + "bevy_utils", +] + [[package]] name = "jni" version = "0.19.0" @@ -1389,6 +1695,12 @@ dependencies = [ "ttf-parser", ] +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + [[package]] name = "parking_lot" version = "0.11.2" @@ -1761,15 +2073,23 @@ name = "sketchrs" version = "0.1.0" dependencies = [ "argmin", + "bevy_ecs", "criterion", "eframe", "indexmap", + "iyes_loopless", "levenberg-marquardt", "nalgebra", "nalgebra-sparse", "nohash-hasher", ] +[[package]] +name = "slab" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" + [[package]] name = "slog" version = "2.7.0" @@ -1827,6 +2147,9 @@ name = "smallvec" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +dependencies = [ + "serde", +] [[package]] name = "smithay-client-toolkit" @@ -1974,6 +2297,7 @@ version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" dependencies = [ + "indexmap", "serde", ] @@ -2060,12 +2384,28 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom", + "serde", +] + [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + [[package]] name = "walkdir" version = "2.3.2" diff --git a/Cargo.toml b/Cargo.toml index 3fa5218..8bf35b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,8 +7,10 @@ edition = "2021" [dependencies] argmin = { version = "0.5.1", features = ["nalgebra"] } +bevy_ecs = "0.7.0" eframe = { version = "0.18.0", features = [] } indexmap = "1.8.1" +iyes_loopless = "0.5.1" levenberg-marquardt = "0.12.0" nalgebra = "0.30.1" nalgebra-sparse = "0.6.0" diff --git a/src/geometry.rs b/src/geometry.rs index 8299938..da38949 100644 --- a/src/geometry.rs +++ b/src/geometry.rs @@ -1,131 +1,139 @@ -mod entity; mod var; -use self::entity::{EntityId, EntityMap}; +use std::ops::DerefMut; + +use bevy_ecs::{ + entity::Entity, + prelude::Component, + query::QueryEntityError, + system::{Commands, Query, SystemParam}, +}; + pub use self::var::*; pub type Scalar = f64; -#[derive(Clone, Debug)] -pub struct PointEntity { +#[derive(Clone, Debug, Component)] +pub struct Point { pub x: V, pub y: V, } -impl PointEntity { +impl Point { pub fn new(x: V, y: V) -> Self { Self { x, y } } } -impl From<(V, V)> for PointEntity { +impl From<(V, V)> for Point { fn from(p: (V, V)) -> Self { Self { x: p.0, y: p.1 } } } -pub type PointPos = PointEntity; -pub type PointId = EntityId; +pub type PointPos = Point; +pub type PointId = Entity; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Component)] // TODO: segment, ray and full line // TODO: what if start and end are coincident? -pub struct LineEntity

{ +pub struct Line

{ pub start: P, pub end: P, } -impl

LineEntity

{ +impl

Line

{ pub fn new(start: P, end: P) -> Self { Self { start, end } } } -pub type LinePos = LineEntity; -pub type LineId = EntityId; +pub type LinePos = Line; -#[derive(Clone)] +#[derive(Clone, Component)] // TODO: arc. how to represent start and end of arc? // angles, unit vectors, or a point for each? -pub struct CircleEntity { +pub struct Circle { pub center: PointId, pub radius: VarId, } // pub type CirclePos = CircleEntity; -pub type CircleId = EntityId; -#[derive(Clone, Default)] -pub struct SketchEntities { - vars: EntityMap, - points: EntityMap, - lines: EntityMap, - circles: EntityMap, +pub fn insert_point_at(commands: &mut Commands, point: impl Into) -> PointId { + let point = point.into(); + let x = commands.spawn().insert(Var::new_free(point.x)).id(); + let y = commands.spawn().insert(Var::new_free(point.y)).id(); + commands.spawn().insert(Point::new(x, y)).id() } -impl SketchEntities { - pub fn vars(&self) -> &EntityMap { - &self.vars +// TODO: figure out generic for this +#[derive(SystemParam)] +pub struct PointPosQuery<'w, 's> { + vars: Query<'w, 's, &'static Var>, +} + +// type PointPosQuery<'w, 's> = PointPosQueryImpl<'w, 's, &'static Var>; +// type PointPosQueryMut<'w, 's> = PointPosQueryImpl<'w, 's, &'static mut Var>; + +// impl<'w, 's, Q> PointPosQueryImpl<'w, 's, Q> +// where +// Q: WorldQuery + 'static, +// for<'w2, 's2> <::ReadOnlyFetch as Fetch<'w2, 's2>>::Item: Deref, +impl<'w, 's> PointPosQuery<'w, 's> { + pub fn try_get(&self, point: &Point) -> Result, QueryEntityError> { + let x = self.vars.get(point.x)?; + let y = self.vars.get(point.y)?; + Ok(x.merge(*y, PointPos::new)) } - - pub fn insert_var(&mut self, var: Var) -> VarId { - self.vars.insert(var) - } - - pub fn get_var_mut(&mut self, id: VarId) -> Option<&mut Var> { - self.vars.get_mut(id) - } - - pub fn points(&self) -> &EntityMap { - &self.points - } - - pub fn insert_point(&mut self, point: PointEntity) -> PointId { - assert!(self.vars.contains(point.x)); - assert!(self.vars.contains(point.y)); - self.points.insert(point) - } - - pub fn insert_point_at(&mut self, point: impl Into) -> PointId { - let point = point.into(); - let x = self.insert_var(Var::new_free(point.x)); - let y = self.insert_var(Var::new_free(point.y)); - self.insert_point(PointEntity::new(x, y)) - } - - fn remove_point(&mut self, point_id: PointId) -> bool { - // TODO: check if used by any line - self.points.remove(point_id) - } - - pub fn point_pos(&self, point: &PointEntity) -> Var { - // TODO: error handling? - let x = self.vars.get(point.x).unwrap(); - let y = self.vars.get(point.y).unwrap(); - x.merge(*y, PointPos::new) - } - - pub fn lines(&self) -> &EntityMap { - &self.lines - } - - pub fn insert_line(&mut self, line: LineEntity) -> LineId { - assert!(self.points.contains(line.start)); - assert!(self.points.contains(line.end)); - self.lines.insert(line) - } - - fn remove_line(&mut self, line_id: LineId) -> bool { - self.lines.remove(line_id) - } - - pub fn line_pos(&self, line: &LineEntity) -> Var { - // TODO: error handling? - let start = self.points.get(line.start).unwrap(); - let start = self.point_pos(start); - - let end = self.points.get(line.end).unwrap(); - let end = self.point_pos(end); - start.merge(end, LinePos::new) + pub fn get(&self, point: &Point) -> Var { + self.try_get(point).unwrap() + } +} + +#[derive(SystemParam)] +pub struct PointPosQueryMut<'w, 's> { + vars: Query<'w, 's, &'static mut Var>, +} + +impl<'w, 's> PointPosQueryMut<'w, 's> +// impl<'w, 's, R> PointPosQueryImpl<'w, 's, Query<'w, 's, R>> +// where +// R: WorldQuery, +// for<'w2, 's2> <::Fetch as Fetch<'w2, 's2>>::Item: DerefMut, +{ + pub fn get(&self, point: &Point) -> Result, QueryEntityError> { + let x = self.vars.get(point.x)?; + let y = self.vars.get(point.y)?; + Ok(x.merge(*y, PointPos::new)) + } + + pub fn set(&mut self, point: &Point, move_to: PointPos) -> Result<(), QueryEntityError> { + for (id, val) in [(point.x, move_to.x), (point.y, move_to.y)] { + self.vars.get_mut(id)?.deref_mut().value = val; + } + Ok(()) + } +} + +#[derive(SystemParam)] +pub struct LinePosQuery<'w, 's> { + point_pos: PointPosQuery<'w, 's>, + points: Query<'w, 's, &'static Point>, +} + +impl<'w, 's> LinePosQuery<'w, 's> { + pub fn get(&self, line: &Line) -> Var { + self.try_get(line).unwrap() + } + + pub fn try_get(&self, line: &Line) -> Result, QueryEntityError> { + // TODO: error handling? + let start = self.points.get(line.start)?; + let start = self.point_pos.try_get(start)?; + + let end = self.points.get(line.end)?; + let end = self.point_pos.try_get(end)?; + Ok(start.merge(end, LinePos::new)) } } diff --git a/src/geometry/entity.rs b/src/geometry/entity.rs deleted file mode 100644 index 39eebcd..0000000 --- a/src/geometry/entity.rs +++ /dev/null @@ -1,117 +0,0 @@ -use std::marker::PhantomData; - -use indexmap::IndexMap; - -// HashMap with fast no-op hash function and preserves insertion order -type IntIndexMap = IndexMap>; - -pub struct EntityId(u32, PhantomData); - -impl Default for EntityId { - fn default() -> Self { - Self(1, Default::default()) - } -} - -impl nohash_hasher::IsEnabled for EntityId {} - -impl EntityId { - pub fn id(&self) -> u32 { - self.0 - } - - fn take_next(&mut self) -> Self { - let id = *self; - self.0 += 1; - id - } - - fn invalid() -> Self { - Self(u32::MAX, Default::default()) - } -} - -#[derive(Clone)] -pub struct EntityMap { - map: IntIndexMap, T>, - next_id: EntityId, -} - -impl EntityMap { - pub fn insert(&mut self, entity: T) -> EntityId { - let id = self.next_id.take_next(); - self.map.insert(id, entity); - id - } - - pub fn contains(&self, id: EntityId) -> bool { - self.map.contains_key(&id) - } - - pub fn remove(&mut self, id: EntityId) -> bool { - self.map.remove(&id).is_some() - } - - pub fn get(&self, id: EntityId) -> Option<&T> { - self.map.get(&id) - } - - pub fn get_mut(&mut self, id: EntityId) -> Option<&mut T> { - self.map.get_mut(&id) - } - - pub fn iter(&self) -> impl Iterator, &T)> { - self.map.iter().map(|(id, v)| (*id, v)) - } -} - -// Need to reimplement all of this without the T bound :/ - -impl PartialEq for EntityId { - fn eq(&self, other: &Self) -> bool { - self.0 == other.0 && self.1 == other.1 - } -} - -impl Eq for EntityId {} - -impl PartialOrd for EntityId { - fn partial_cmp(&self, other: &Self) -> Option { - self.0.partial_cmp(&other.0) - } -} - -impl Ord for EntityId { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.0.cmp(&other.0) - } -} - -impl Clone for EntityId { - fn clone(&self) -> Self { - Self(self.0.clone(), self.1.clone()) - } -} - -impl Copy for EntityId {} - -impl std::hash::Hash for EntityId { - fn hash(&self, state: &mut H) { - self.0.hash(state); - } -} - -impl std::fmt::Debug for EntityId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("EntityId").field(&self.0).finish() - } -} - -impl Default for EntityMap { - fn default() -> Self { - Self { - map: Default::default(), - next_id: Default::default(), - } - } -} diff --git a/src/geometry/var.rs b/src/geometry/var.rs index 0cfa29d..2f477a1 100644 --- a/src/geometry/var.rs +++ b/src/geometry/var.rs @@ -1,4 +1,6 @@ -use super::{EntityId, Scalar}; +use bevy_ecs::{prelude::Component, entity::Entity}; + +use super::Scalar; #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum VarStatus { @@ -20,7 +22,7 @@ impl VarStatus { } } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Component)] pub struct Var { pub value: T, pub status: VarStatus, @@ -59,4 +61,4 @@ impl std::ops::DerefMut for Var { } } -pub type VarId = EntityId; +pub type VarId = Entity; diff --git a/src/main.rs b/src/main.rs index 8b0632a..14b33cb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,14 @@ +use bevy_ecs::{ + prelude::*, + system::{Commands, IntoSystem, Query, Res, ResMut, System}, +}; use eframe::{ - egui::{self, CursorIcon, Frame, PointerButton, Sense, Ui}, + egui::{self, CursorIcon, Painter, Response, Sense, Ui}, emath::{Pos2, Rect, RectTransform, Vec2}, epaint::{color::Hsva, Color32, Stroke}, }; -use geometry::{LineEntity, LineId, PointId, PointPos, SketchEntities}; +use geometry::{Line, LinePosQuery, Point, PointId, PointPos, PointPosQuery, PointPosQueryMut}; +use iyes_loopless::prelude::*; mod geometry; mod optimization; @@ -18,7 +23,7 @@ fn main() { ); } -#[derive(PartialEq)] +#[derive(Clone, Copy, PartialEq)] enum Tool { Select, Move, @@ -32,35 +37,29 @@ impl Default for Tool { } } -#[derive(Default)] -struct ViewState { - tool: Tool, - hover_point: Option, - select_point: Option, - hover_line: Option, - select_line: Option, - drag_delta: Vec2, -} +struct ToScreen(RectTransform); -struct MyApp { - state: ViewState, - entities: SketchEntities, -} +impl ToScreen { + fn transform_pos(&self, pos: &PointPos) -> Pos2 { + self.0 * Pos2::new(pos.x as f32, pos.y as f32) + } -impl Default for MyApp { - fn default() -> Self { - let mut entities = SketchEntities::default(); - let p1 = entities.insert_point_at((10., 30.)); - let p2 = entities.insert_point_at((-20., 15.)); - entities.insert_point_at((0., -10.)); - entities.insert_line(LineEntity::new(p1, p2)); - Self { - state: ViewState::default(), - entities, - } + fn inverse_transform(&self, pos: Pos2) -> Pos2 { + self.0.inverse() * pos + } + + fn inverse_transform_to_point(&self, pos: Pos2) -> PointPos { + let pos = self.inverse_transform(pos); + PointPos::new(pos.x as f64, pos.y as f64) } } +#[derive(Component)] +struct Hovered; + +#[derive(Component)] +struct Selected; + fn color_for_var_status(status: geometry::VarStatus) -> Hsva { use geometry::VarStatus::*; match status { @@ -73,13 +72,359 @@ fn color_for_var_status(status: geometry::VarStatus) -> Hsva { const POINT_RADIUS: f32 = 3.0; +fn update_hover_point( + response: Res, + to_screen: Res, + point_pos: PointPosQuery, + points: Query<(PointId, &Point)>, + mut commands: Commands, +) { + points.for_each(|(id, point)| { + let hovered = if let Some(hover_pos) = response.hover_pos() { + let pos = point_pos.get(point); + let center = to_screen.transform_pos(&*pos); + + (hover_pos - center).length() < (POINT_RADIUS * 3.) + } else { + false + }; + + if hovered { + commands.entity(id).insert(Hovered); + } else { + commands.entity(id).remove::(); + } + }); +} + +fn update_hover_line( + response: Res, + to_screen: Res, + lines: Query<(Entity, &Line)>, + line_pos: LinePosQuery, + mut commands: Commands, +) { + lines.for_each(|(id, line)| { + let hovered = if let Some(hover_pos) = response.hover_pos() { + let pos = line_pos.get(line); + let points = [ + to_screen.transform_pos(&pos.start), + to_screen.transform_pos(&pos.end), + ]; + + let b = points[1] - points[0]; + let a = hover_pos - points[0]; + let p = a.dot(b) / b.dot(b); + let perp = a - (p * b); + ((0.)..=(1.)).contains(&p) && perp.length() < 5.0 + } else { + false + }; + + if hovered { + commands.entity(id).insert(Hovered); + } else { + commands.entity(id).remove::(); + } + }); +} + +fn update_cursor_icon( + response: Res, + tool: Res, + hovered: Query<(), With>, + selected: Query<(), With>, +) { + response.ctx.output().cursor_icon = match *tool { + Tool::Select => { + if !hovered.is_empty() { + CursorIcon::PointingHand + } else { + CursorIcon::Default + } + } + Tool::Move => { + if !selected.is_empty() { + CursorIcon::Grabbing + } else if !hovered.is_empty() { + CursorIcon::Grab + } else { + CursorIcon::Move + } + } + Tool::AddPoint => CursorIcon::None, + Tool::AddLine => CursorIcon::None, + }; +} + +fn select_tool( + response: Res, + hovered: Query>, + selected: Query>, + mut commands: Commands, +) { + response.ctx.output().cursor_icon = if !hovered.is_empty() { + CursorIcon::PointingHand + } else { + CursorIcon::Default + }; + + if response.clicked() { + selected.for_each(|selected| { + commands.entity(selected).remove::(); + }); + // TODO: choose which to select + if let Some(hovered) = hovered.iter().next() { + commands.entity(hovered).insert(Selected); + } + } +} + +#[derive(Default)] +struct DragDelta(Vec2); + +// TODO: move other entities +fn move_tool( + response: Res, + to_screen: Res, + mut drag_delta: Local, + mut point_pos: PointPosQueryMut, + hovered: Query<(Entity, &Point), With>, + selected: Query<(Entity, &Point), With>, + mut commands: Commands, +) { + let hover_pos = response.hover_pos().unwrap(); + + response.ctx.output().cursor_icon = if !selected.is_empty() { + CursorIcon::Grabbing + } else if !hovered.is_empty() { + CursorIcon::Grab + } else { + CursorIcon::Move + }; + + let selected = if response.drag_started() { + // TODO: choose which to select + let to_select = hovered.iter().next(); + if let Some(hovered) = to_select { + commands.entity(hovered.0).insert(Selected); + } + to_select + } else if response.drag_released() { + selected.for_each(|selected| { + commands.entity(selected.0).remove::(); + }); + drag_delta.0 = Vec2::ZERO; + None + } else { + selected.iter().next() + }; + + if let Some((_, point)) = selected { + if response.drag_started() { + let drag_point_pos = point_pos.get(point).unwrap(); + drag_delta.0 = hover_pos - to_screen.transform_pos(&*drag_point_pos); + } + let move_to = to_screen.inverse_transform_to_point(hover_pos - drag_delta.0); + point_pos.set(point, move_to).unwrap(); + } +} + +fn add_point(commands: &mut Commands, pos: Pos2, to_screen: &ToScreen) -> PointId { + let point_pos = to_screen.inverse_transform(pos); + let point_pos = (point_pos.x as f64, point_pos.y as f64); + geometry::insert_point_at(commands, point_pos) +} + +fn add_point_tool( + response: Res, + to_screen: Res, + painter: ResMut, + mut commands: Commands, +) { + let hover_pos = response.hover_pos().unwrap(); + if response.clicked() { + add_point(&mut commands, hover_pos, &*to_screen); + } else { + painter.circle_filled(hover_pos, POINT_RADIUS, Color32::WHITE); + } +} + +fn add_line_tool( + response: Res, + to_screen: Res, + painter: ResMut, + hovered: Query, With)>, + selected: Query<(Entity, &Point), With>, + point_pos: PointPosQuery, + mut commands: Commands, +) { + let hover_pos = response.hover_pos().unwrap(); + + match (selected.iter().next(), response.clicked()) { + (None, false) => { + painter.circle_filled(hover_pos, POINT_RADIUS, Color32::DARK_GRAY); + } + (None, true) => { + let point_id = match hovered.iter().next() { + Some(hovered) => hovered, + None => add_point(&mut commands, hover_pos, &*to_screen), + }; + commands.entity(point_id).insert(Selected); + } + (Some(start_point), false) => { + let start_point_pos = point_pos.get(start_point.1); + let points = [to_screen.transform_pos(&start_point_pos), hover_pos]; + + let stroke = Stroke::new(2.0, Color32::DARK_GRAY); + + painter.line_segment(points, stroke); + + painter.circle_filled(hover_pos, POINT_RADIUS, Color32::DARK_GRAY); + } + (Some(start_point), true) => { + // TODO: add point if no hover point + let end_point = hovered + .iter() + .next() + .unwrap_or_else(|| add_point(&mut commands, hover_pos, &*to_screen)); + + let line = Line::new(start_point.0, end_point); + commands.spawn().insert(line); + + selected.for_each(|selected| { + commands.entity(selected.0).remove::(); + }); + } + } +} + +fn is_hovered(response: Res) -> bool { + response.hover_pos().is_some() +} + +fn is_tool_active(tool: Tool) -> impl System { + IntoSystem::into_system(move |active_tool: Res| *active_tool == tool) +} + +fn paint_lines( + to_screen: Res, + painter: ResMut, + lines: Query<(Entity, &Line)>, + hovered: Query<(), With>, + selected: Query<(), With>, + line_pos: LinePosQuery, +) { + lines.for_each(|(id, line)| { + let pos = line_pos.get(line); + let points = [ + to_screen.transform_pos(&pos.start), + to_screen.transform_pos(&pos.end), + ]; + + let mut color = color_for_var_status(pos.status); + color.v -= 0.6; + if hovered.contains(id) || selected.contains(id) { + color.s -= 0.8; + } + + let stroke = Stroke::new(2.0, color); + + painter.line_segment(points, stroke); + }); +} + +fn paint_points( + to_screen: Res, + painter: ResMut, + points: Query<(Entity, &Point)>, + hovered: Query<(), With>, + selected: Query<(), With>, + point_pos: PointPosQuery, +) { + points.for_each(|(id, point)| { + let pos = point_pos.get(point); + + let center = to_screen.transform_pos(&*pos); + + let color = color_for_var_status(pos.status); + let stroke = if selected.contains(id) || hovered.contains(id) { + // color.s -= 0.8; + Stroke::new(1.0, Color32::WHITE) + } else { + Stroke::default() + }; + painter.circle(center, POINT_RADIUS, color, stroke); + }); +} + +struct MyApp { + world: World, + show_entities_stage: SystemStage, +} + +fn init(mut commands: Commands) { + let p1 = geometry::insert_point_at(&mut commands, (10., 30.)); + let p2 = geometry::insert_point_at(&mut commands, (-20., 15.)); + geometry::insert_point_at(&mut commands, (0., -10.)); + commands.spawn().insert(Line::new(p1, p2)); +} + +impl Default for MyApp { + fn default() -> Self { + let mut world = World::default(); + + world.init_resource::(); + + let mut init_stage = SystemStage::single_threaded().with_system(init); + init_stage.run(&mut world); + Self { + world, + show_entities_stage: Self::create_show_entities_stage(), + } + } +} + impl MyApp { + fn create_show_entities_stage() -> SystemStage { + #[derive(SystemLabel, Debug, Clone, PartialEq, Eq, Hash)] + struct InputGroup; + + #[derive(SystemLabel, Debug, Clone, PartialEq, Eq, Hash)] + struct ToolsGroup; + + SystemStage::single_threaded() + .with_system_set( + ConditionSet::new() + .label("input") + .with_system(update_hover_point) + .with_system(update_hover_line) + .into(), + ) + .with_system_set( + ConditionSet::new() + .label("tools") + .after("input") + .run_if(is_hovered) + .with_system(update_cursor_icon) + .with_system(select_tool.run_if(is_tool_active(Tool::Select))) + .with_system(move_tool.run_if(is_tool_active(Tool::Move))) + .with_system(add_point_tool.run_if(is_tool_active(Tool::AddPoint))) + .with_system(add_line_tool.run_if(is_tool_active(Tool::AddLine))) + .into(), + ) + .with_system(paint_lines.label("paint_lines").after("tools")) // TODO order dependency + .with_system(paint_points.after("paint_lines")) + } + fn show_toolbar(&mut self, ui: &mut Ui) { ui.heading("sketchrs"); ui.horizontal(|ui| { ui.label("Tool: "); - let mut tool = &mut self.state.tool; + + let tool = &mut *self.world.resource_mut::(); + ui.selectable_value(tool, Tool::Select, "Select"); ui.selectable_value(tool, Tool::Move, "Move"); ui.selectable_value(tool, Tool::AddPoint, "+ Point"); @@ -88,200 +433,44 @@ impl MyApp { } fn show_entities(&mut self, ui: &mut Ui) { - let mut state = &mut self.state; - - let sense = match state.tool { + let sense = match *self.world.resource::() { Tool::Move => Sense::drag(), Tool::Select | Tool::AddPoint | Tool::AddLine => Sense::click(), }; let (response, painter) = ui.allocate_painter(ui.available_size(), sense); - let ctx = response.ctx.clone(); - let to_screen = RectTransform::from_to( + let to_screen = ToScreen(RectTransform::from_to( Rect::from_center_size(Pos2::ZERO, response.rect.size() / 2.0), response.rect, - ); + )); + self.world.insert_resource(to_screen); + self.world.insert_resource(response); + self.world.insert_resource(painter); - let transform_pos = |pos: &PointPos| to_screen * Pos2::new(pos.x as f32, pos.y as f32); + self.show_entities_stage.run(&mut self.world); - state.hover_point = None; - state.hover_line = None; - - if let Some(hover_pos) = response.hover_pos() { - for (id, point) in self - .entities - .points() - .iter() - .filter(|(id, _)| Some(*id) != state.select_point) - { - let pos = self.entities.point_pos(point); - let center = transform_pos(&*pos); - - if (hover_pos - center).length() < (POINT_RADIUS * 3.) { - state.hover_point = Some(id); - break; - } - } - - for (id, line) in self.entities.lines().iter() { - let pos = self.entities.line_pos(line); - let points = [transform_pos(&pos.start), transform_pos(&pos.end)]; - - let b = points[1] - points[0]; - let a = hover_pos - points[0]; - let p = a.dot(b) / b.dot(b); - let perp = a - (p * b); - let hovered = ((0.)..=(1.)).contains(&p) && perp.length() < 5.0; - - if hovered { - state.hover_line = Some(id); - break; - } - } - - ctx.output().cursor_icon = match state.tool { - Tool::Select => { - if state.hover_point.is_some() { - CursorIcon::PointingHand - } else { - CursorIcon::Default - } - } - Tool::Move => { - if state.select_point.is_some() { - CursorIcon::Grabbing - } else if state.hover_point.is_some() { - CursorIcon::Grab - } else { - CursorIcon::Move - } - } - Tool::AddPoint => CursorIcon::None, - Tool::AddLine => CursorIcon::None, - }; - - let mut add_point = || { - let point_pos = to_screen.inverse() * hover_pos; - let point_pos = (point_pos.x as f64, point_pos.y as f64); - self.entities.insert_point_at(point_pos) - }; - - match state.tool { - Tool::Select => { - if response.clicked() { - state.select_point = None; - state.select_line = None; - if state.hover_point.is_some() { - state.select_point = state.hover_point; - } else if state.hover_line.is_some() { - state.select_line = state.hover_line; - } - } - } - Tool::Move => { - let drag_started = response.drag_started(); - if drag_started { - state.select_point = state.hover_point; - } else if response.drag_released() { - state.select_point = None; - state.drag_delta = Vec2::ZERO; - } - - if let Some(point_id) = state.select_point { - let point = self.entities.points().get(point_id).unwrap(); - if drag_started { - let point_pos = self.entities.point_pos(point); - state.drag_delta = hover_pos - transform_pos(&*point_pos); - } - let move_to = to_screen.inverse() * (hover_pos - state.drag_delta); - for (id, val) in [(point.x, move_to.x), (point.y, move_to.y)] { - self.entities.get_var_mut(id).unwrap().value = val as f64; - } - } - } - Tool::AddPoint => { - if response.clicked() { - add_point(); - } else { - painter.circle_filled(hover_pos, POINT_RADIUS, Color32::WHITE); - } - } - Tool::AddLine => { - match (state.select_point, response.clicked()) { - (Some(start_point_id), true) => { - // TODO: add point if no hover point - let end_point = state.hover_point.unwrap_or_else(add_point); - - let line = LineEntity::new(start_point_id, end_point); - self.entities.insert_line(line); - - state.select_point = None; - } - (None, true) => { - state.select_point = Some(state.hover_point.unwrap_or_else(add_point)); - } - (Some(first_point_id), false) => { - let first_point = self.entities.points().get(first_point_id).unwrap(); - let first_point_pos = self.entities.point_pos(first_point); - let points = [transform_pos(&first_point_pos), hover_pos]; - - let stroke = Stroke::new(2.0, Color32::DARK_GRAY); - - painter.line_segment(points, stroke); - - painter.circle_filled(hover_pos, POINT_RADIUS, Color32::DARK_GRAY); - } - (None, false) => { - painter.circle_filled(hover_pos, POINT_RADIUS, Color32::DARK_GRAY); - } - } - } - } - } - - for (id, line) in self.entities.lines().iter() { - let pos = self.entities.line_pos(line); - let points = [transform_pos(&pos.start), transform_pos(&pos.end)]; - - let mut color = color_for_var_status(pos.status); - color.v -= 0.6; - if state.hover_point.is_none() - && (state.select_point.is_none() || state.tool != Tool::Move) - && (Some(id) == state.hover_line || Some(id) == state.select_line) - { - color.s -= 0.8; - } - - let stroke = Stroke::new(2.0, color); - - painter.line_segment(points, stroke); - } - - for (id, point) in self.entities.points().iter() { - let pos = self.entities.point_pos(point); - - let center = transform_pos(&*pos); - - let mut color = color_for_var_status(pos.status); - let stroke = match (state.select_point, state.hover_point) { - (Some(sid), _) | (_, Some(sid)) if id == sid => { - // color.s -= 0.8; - Stroke::new(1.0, Color32::WHITE) - } - _ => Stroke::default(), - }; - painter.circle(center, POINT_RADIUS, color, stroke); - } + self.world.remove_resource::(); + self.world.remove_resource::(); + self.world.remove_resource::(); } fn show_side_panel(&mut self, ui: &mut Ui) { - ui.vertical(|ui| match self.state.tool { + let tool = *self.world.resource::(); + ui.vertical(|ui| match tool { Tool::Select => { - if let Some(point_id) = self.state.select_point { - ui.label(format!("Selected point {}", point_id.id())); - } else if let Some(line_id) = self.state.select_line { - ui.label(format!("Selected line {}", line_id.id())); - } else { + let mut selected = self + .world + .query_filtered::<(Entity, Option<&Point>, Option<&Line>), With>(); + let mut count = 0; + selected.for_each(&self.world, |(id, point, line)| { + count += 1; + if point.is_some() { + ui.label(format!("Selected point {}", id.id())); + } else if line.is_some() { + ui.label(format!("Selected line {}", id.id())); + } + }); + if count == 0 { ui.label("No selection"); } }