diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index 0e8687c9..4a807061 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -59,6 +59,8 @@ jobs: run: wget https://apt.llvm.org/llvm.sh && chmod +x llvm.sh && sudo ./llvm.sh 10 - name: Install ImGui dependencies run: sudo apt install libxcb-shape0-dev libxcb-xfixes0-dev + - name: Install Native File Dialog dependencies + run: sudo apt-get install libgtk-3-dev - name: Build NDCell run: cargo build --release - name: Upload executable diff --git a/CHANGELOG.md b/CHANGELOG.md index 32c51b46..47ba2361 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,32 +4,47 @@ All notable changes to NDCell will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), except for minor stylistic changes to organize features and accomodate named versions. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) with respect to the Rust API for `ndcell_core`, the NDCA API for `ndcell_lang`, and the combined Lua/NDCA API for `ndcell_ui`, the main application. -## [Unreleased] +## [0.2.0] Blinker (2020-02-21) + +![Blinker](https://user-images.githubusercontent.com/6060305/108616710-f3316780-73dd-11eb-858f-1cda97cad993.png) ### Added - **Simulation** + - 3D rendering and simulation - Advance one generation (Space) - Advance one step (Tab) - - 3D rendering and simulation +- **Selection** + - Added edge resize indicator + - Cancel selection drag (Esc) - **Navigation** - 3D orbit (right mouse drag) - 3D pan (///, W/A/S/D, or middle mouse drag) - 3D pan horizontally (middle mouse drag with Shift) + - Zoom (right mouse drag with Ctrl) +- **GUI** + - Load/save file ### Changed - **Simulation** - Cells align better to pixel boundaries when zoomed out, appearing crisper - Optimized 2D rendering of empty areas +- **Selection** + - Selection edge resizing now clamps to the opposite corner - **GUI** - - Rename "UPS" (updates per second) to "step/sec" (steps per second) + - Disabled rounded window borders - Display "RUNNING" or "STEPPING" accordingly instead of "SIMULATING" + - Relabeled "Trigger garbage collection" button to "Clear cache" + - Replaced inaccurate maximum simulation speed with average simulation time. +- Tweaked colors ### Fixed - Changing the step size while the simulation is running now takes effect immediately ([#6][i6]) - Selected cells no longer appear to be tiled infinitely +- Touchpad scrolling now zooms in/out at a reasonable pace +- Crash when pressing an exotic mouse button [i6]: https://github.com/HactarCE/NDCell/issues/6 diff --git a/Cargo.lock b/Cargo.lock index e029a7be..896b0188 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -55,6 +55,15 @@ version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee67c11feeac938fae061b232e38e0b6d94f97a9df10e6271319325ac4c56a86" +[[package]] +name = "approx" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0e60b75072ecd4168020818c0107f2857bb6c4e64252d8d3983f6263b40a5c3" +dependencies = [ + "num-traits", +] + [[package]] name = "approx" version = "0.4.0" @@ -128,7 +137,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "473fc6b38233f9af7baa94fb5852dca389e3d95b8e21c8e3719301462c5d9faf" dependencies = [ - "lazy_static", + "lazy_static 1.4.0", "memchr", "regex-automata", "serde", @@ -198,10 +207,16 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a98d30140e3296250832bbaaff83b27dcd6fa3cc70fb6f1f3e5c9c0023b5317" dependencies = [ - "approx", + "approx 0.4.0", "num-traits", ] +[[package]] +name = "chlorine" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd650552110e39b7c5058986cf177decd3365841836578ac50a286094eac0be6" + [[package]] name = "chrono" version = "0.4.19" @@ -320,7 +335,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4ffc801dacf156c5854b9df4f425a626539c3a6ef7893cc0c5084a23f0b6c59" dependencies = [ "atty", - "lazy_static", + "lazy_static 1.4.0", "winapi 0.3.9", ] @@ -430,7 +445,7 @@ dependencies = [ "criterion-plot", "csv", "itertools 0.9.0", - "lazy_static", + "lazy_static 1.4.0", "num-traits", "oorandom", "plotters", @@ -484,7 +499,7 @@ dependencies = [ "cfg-if 1.0.0", "const_fn", "crossbeam-utils", - "lazy_static", + "lazy_static 1.4.0", "memoffset", "scopeguard", ] @@ -497,7 +512,16 @@ checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d" dependencies = [ "autocfg", "cfg-if 1.0.0", - "lazy_static", + "lazy_static 1.4.0", +] + +[[package]] +name = "css-color-parser" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ccb6ce7ef97e6dc6e575e51b596c9889a5cc88a307b5ef177d215c61fd7581d" +dependencies = [ + "lazy_static 0.1.16", ] [[package]] @@ -693,7 +717,7 @@ dependencies = [ "fnv", "gl_generator", "glutin", - "lazy_static", + "lazy_static 1.4.0", "memoffset", "smallvec", "takeable-option", @@ -714,7 +738,7 @@ dependencies = [ "glutin_gles2_sys", "glutin_glx_sys", "glutin_wgl_sys", - "lazy_static", + "lazy_static 1.4.0", "libloading", "log", "objc", @@ -794,21 +818,20 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "imgui" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92a0077d3bb882960467aed0bc6eaf5d4033cb9b61bfdbb99c32d1288380032f" +checksum = "24cfcf6e3326886321c5d637caf1ce217006651059015fae372b1c49c0e722b2" dependencies = [ "bitflags", - "glium", "imgui-sys", "parking_lot 0.11.1", ] [[package]] name = "imgui-glium-renderer" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0755707343b50c7710ea5ededb7c955d2bceff7d38b515f7e6e0c7eeca04ca" +checksum = "8c295c510c0d7209cf2304741808425bcd8e421142679d2448a38d4aa51762f3" dependencies = [ "glium", "imgui", @@ -816,18 +839,19 @@ dependencies = [ [[package]] name = "imgui-sys" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0522b693da8a64322afbb32c63c04f39d9b9435cc75199d630207eee48886fc1" +checksum = "85ca00be6b78bf02b57e91468cf19d08dfcc11d0fb3c2f3dc491c29404d8d330" dependencies = [ "cc", + "chlorine", ] [[package]] name = "imgui-winit-support" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0669ee7f52b80aa33d5f45507790223380c253a8fd981ddf67d1427ab8ebc8bc" +checksum = "d632440e05c964e8a7f00f2659c4f71c97897d8c38a77a0c2dc1f3fe8d632208" dependencies = [ "imgui", "winit", @@ -930,6 +954,12 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "lazy_static" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf186d1a8aa5f5bee5fd662bc9c1b949e0259e1bcc379d1f006847b0080c7417" + [[package]] name = "lazy_static" version = "1.4.0" @@ -974,7 +1004,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9109e19fbfac3458f2970189719fa19f1007c6fd4e08c44fdebf4be0ddbe261d" dependencies = [ "cc", - "lazy_static", + "lazy_static 1.4.0", "libc", "regex", "semver", @@ -1110,36 +1140,40 @@ dependencies = [ [[package]] name = "ndcell" -version = "0.2.0-dev" +version = "0.2.0" dependencies = [ "anyhow", "cgmath", "clipboard", "colorous", + "css-color-parser", "enum_dispatch", "glium", "imgui", "imgui-glium-renderer", "imgui-winit-support", "itertools 0.10.0", - "lazy_static", + "lazy_static 1.4.0", "log", "mimalloc", "ndcell_core", "ndcell_lang", + "nfd2", + "palette", "parking_lot 0.11.1", "proptest", "send_wrapper", "simple_logger", + "sloth", ] [[package]] name = "ndcell_core" -version = "0.2.0-dev" +version = "0.2.0" dependencies = [ "criterion", "itertools 0.10.0", - "lazy_static", + "lazy_static 1.4.0", "noisy_float", "num", "parking_lot 0.11.1", @@ -1155,7 +1189,7 @@ version = "0.1.0" dependencies = [ "inkwell", "itertools 0.10.0", - "lazy_static", + "lazy_static 1.4.0", "ndcell_core", "proptest", "regex", @@ -1182,7 +1216,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bdf399b8b7a39c6fb153c4ec32c72fd5fe789df24a647f229c239aa7adb15241" dependencies = [ - "lazy_static", + "lazy_static 1.4.0", "libc", "log", "ndk", @@ -1220,6 +1254,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "nfd2" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cedbdc1ecacd67f890b05bf38d268cf42dd070a4b44e82b89c4757998e7250d5" +dependencies = [ + "cc", +] + [[package]] name = "nix" version = "0.18.0" @@ -1424,6 +1467,30 @@ dependencies = [ "ttf-parser", ] +[[package]] +name = "palette" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a05c0334468e62a4dfbda34b29110aa7d70d58c7fdb2c9857b5874dd9827cc59" +dependencies = [ + "approx 0.3.2", + "num-traits", + "palette_derive", + "phf", + "phf_codegen", +] + +[[package]] +name = "palette_derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b4b5f600e60dd3a147fb57b4547033d382d1979eb087af310e91cb45a63b1f4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "parking_lot" version = "0.10.2" @@ -1479,6 +1546,44 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared", + "rand 0.7.3", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + [[package]] name = "pkg-config" version = "0.3.19" @@ -1530,7 +1635,7 @@ dependencies = [ "bit-set", "bitflags", "byteorder", - "lazy_static", + "lazy_static 1.4.0", "num-traits", "quick-error", "rand 0.7.3", @@ -1567,6 +1672,7 @@ dependencies = [ "rand_chacha 0.2.2", "rand_core 0.5.1", "rand_hc 0.2.0", + "rand_pcg", ] [[package]] @@ -1637,6 +1743,15 @@ dependencies = [ "rand_core 0.6.1", ] +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + [[package]] name = "rand_xorshift" version = "0.2.0" @@ -1676,7 +1791,7 @@ dependencies = [ "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "lazy_static", + "lazy_static 1.4.0", "num_cpus", ] @@ -1857,7 +1972,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" dependencies = [ - "lazy_static", + "lazy_static 1.4.0", "libc", ] @@ -1874,12 +1989,24 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "siphasher" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa8f3741c7372e75519bd9346068370c9cdaabcc1f9599cbcf2a2719352286b7" + [[package]] name = "slab" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" +[[package]] +name = "sloth" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7c30017141f49ee0d60d30c378052d8cccee245f0ea2bb37492a26ac61a60f8" + [[package]] name = "smallvec" version = "1.6.1" @@ -1896,7 +2023,7 @@ dependencies = [ "bitflags", "calloop", "dlib", - "lazy_static", + "lazy_static 1.4.0", "log", "memmap2", "nix", @@ -1977,7 +2104,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb9bc092d0d51e76b2b19d9d85534ffc9ec2db959a2523cdae0697e2972cd447" dependencies = [ - "lazy_static", + "lazy_static 1.4.0", ] [[package]] @@ -2082,7 +2209,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1114f89ab1f4106e5b55e688b828c0ab0ea593a1ea7c094b141b14cbaaec2d62" dependencies = [ "bumpalo", - "lazy_static", + "lazy_static 1.4.0", "log", "proc-macro2", "quote", @@ -2198,7 +2325,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6793834e0c35d11fd96a97297abe03d37be627e1847da52e17d7e0e3b51cc099" dependencies = [ "dlib", - "lazy_static", + "lazy_static 1.4.0", "pkg-config", ] @@ -2268,7 +2395,7 @@ dependencies = [ "core-video-sys", "dispatch", "instant", - "lazy_static", + "lazy_static 1.4.0", "libc", "log", "mio", @@ -2311,7 +2438,7 @@ version = "2.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf981e3a5b3301209754218f962052d4d9ee97e478f4d26d4a6eced34c1fef8" dependencies = [ - "lazy_static", + "lazy_static 1.4.0", "libc", "maybe-uninit", "pkg-config", diff --git a/TODO.md b/TODO.md index f4b93799..830b9c5f 100644 --- a/TODO.md +++ b/TODO.md @@ -26,7 +26,7 @@ - [ ] Selecting - [ ] Resize selection - [ ] Move selection - - [ ] Drawing + - [x] Drawing - [ ] Improve UI - [ ] Better error reporting - [ ] RLE loading diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 211b51eb..34d476f6 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -4,13 +4,16 @@ All notable changes to `ndcell_core` will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [0.2.0] - 2020-02-21 ### Added +- `Layer` method `round_rect_with_base_pos()` - `NdVec` methods `min_component()` and `max_component()` - `NdRect` method `span_rects()` - Re-export of `num` modules `cast`, `iter`, and `pow` from `crate::num` +- Implementation of `std::error::Error` for `CaFormatError` +- Implementation of `std::iter::{Product, Sum}` for `NdVec` ### Changed diff --git a/core/Cargo.toml b/core/Cargo.toml index b28c9b7c..ac9cf36c 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ndcell_core" -version = "0.2.0-dev" +version = "0.2.0" authors = ["HactarCE <6060305+HactarCE@users.noreply.github.com>"] edition = "2018" diff --git a/core/src/io/mod.rs b/core/src/io/mod.rs index e6efc437..4671748f 100644 --- a/core/src/io/mod.rs +++ b/core/src/io/mod.rs @@ -41,6 +41,7 @@ pub enum CaFormatError { RleError(RleError), MacrocellError(MacrocellError), } +impl std::error::Error for CaFormatError {} impl From for CaFormatError { fn from(e: RleError) -> Self { Self::RleError(e) diff --git a/core/src/ndrect/mod.rs b/core/src/ndrect/mod.rs index f3bdbe45..bb3a30a0 100644 --- a/core/src/ndrect/mod.rs +++ b/core/src/ndrect/mod.rs @@ -132,7 +132,7 @@ where } /// Creates an `NdRect` spanning both of the given rectangles (inclusive). - pub fn span_rects(a: Self, b: Self) -> Self { + pub fn span_rects(a: &Self, b: &Self) -> Self { Self::span( NdVec::min(&a.min(), &b.min()), NdVec::max(&a.max(), &b.max()), diff --git a/core/src/ndtree/node/layer.rs b/core/src/ndtree/node/layer.rs index dbd6471b..3bc398d7 100644 --- a/core/src/ndtree/node/layer.rs +++ b/core/src/ndtree/node/layer.rs @@ -269,6 +269,16 @@ impl Layer { let len = self.big_len(); rect.div_outward(&len) * &len } + /// Rounds the given rectangle outward to the next node boundaries at this + /// layer, given a global ND-tree base position. + #[inline] + pub fn round_rect_with_base_pos( + self, + rect: BigRect, + base_pos: &BigVec, + ) -> BigRect { + self.round_rect(&(rect - base_pos)) + base_pos + } /// Returns the layer of the largest layer boundary a vector points to, or /// `None` if it is the origin vector (largest aligned layer would be diff --git a/core/src/ndtree/node/raw.rs b/core/src/ndtree/node/raw.rs index 43253542..a7fa049d 100644 --- a/core/src/ndtree/node/raw.rs +++ b/core/src/ndtree/node/raw.rs @@ -458,8 +458,14 @@ impl RawNode { /// Automatically converts the argument to a `BigUint` if necessary. fn set_pop_small(&self, pop: usize) { let value_to_store = (pop << 1) | 1; + // Does `pop` fit inside 63 bits? If not, we'll need to convert to + // `BigUint`. if value_to_store >> 1 == pop { - self.population.compare_and_swap(0, (pop << 1) | 1, Relaxed); + // If this fails, we don't care. That just means some other thread + // computed the population before we did. + let _ = self + .population + .compare_exchange(0, (pop << 1) | 1, Relaxed, Relaxed); } else { self.set_pop_big(pop.into()); } @@ -468,10 +474,11 @@ impl RawNode { fn set_pop_big(&self, pop: BigUint) { // Put it on the heap and leak it. let new_pop_ptr = Box::into_raw(Box::new(pop)); - let old = self + if self .population - .compare_and_swap(0, new_pop_ptr as usize, Relaxed); - if old != 0 { + .compare_exchange(0, new_pop_ptr as usize, Relaxed, Relaxed) + .is_err() + { // The swap was not successful, so drop `pop_ptr` because the // it's not in `self.population`. unsafe { std::ptr::drop_in_place(new_pop_ptr) }; diff --git a/core/src/ndvec/ops/iter.rs b/core/src/ndvec/ops/iter.rs new file mode 100644 index 00000000..1e4500db --- /dev/null +++ b/core/src/ndvec/ops/iter.rs @@ -0,0 +1,31 @@ +//! Operations on iterators over `NdVec`s. + +use std::iter::{Product, Sum}; + +use super::*; + +impl, N: NdVecNum, X> Product for NdVec +where + NdVec: MulAssign, +{ + fn product>(iter: I) -> Self { + let mut ret = Self::origin(); + for x in iter { + ret *= x; + } + ret + } +} + +impl, N: NdVecNum, X> Sum for NdVec +where + NdVec: AddAssign, +{ + fn sum>(iter: I) -> Self { + let mut ret = Self::origin(); + for x in iter { + ret += x; + } + ret + } +} diff --git a/core/src/ndvec/ops/mod.rs b/core/src/ndvec/ops/mod.rs index 0b6a946c..9be8dbea 100644 --- a/core/src/ndvec/ops/mod.rs +++ b/core/src/ndvec/ops/mod.rs @@ -9,4 +9,5 @@ mod cmp; mod divmod; mod float; mod int; +mod iter; mod signed; diff --git a/docs/conf.py b/docs/conf.py index bfa17146..960dfdcc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -20,7 +20,7 @@ # -- Project information ----------------------------------------------------- project = 'NDCell' -copyright = '2020, HactarCE' +copyright = '2021, HactarCE' author = 'HactarCE' diff --git a/docs/custom-rules/rationale.rst b/docs/custom-rules/rationale.rst index dd20ea1b..e4303b89 100644 --- a/docs/custom-rules/rationale.rst +++ b/docs/custom-rules/rationale.rst @@ -1,10 +1,11 @@ -.. _rationale: - ********* Rationale ********* -Most existing cellular automaton simulation programs that support custom rules either require them defined in verbose languages like C or Java or as a list of transitions, which is limiting and hard to read. A custom programming language provides the power of math, logic, and variables with utilities unique to cellular automata, like first-class support for symmetries and masked N-dimensional arrays. +Why make a new programming language? +==================================== + +Most existing cellular automaton simulation programs that support custom rules either require them defined in existing programming languages like C or Java, which tend to be verbose, or as a list of transitions, which is limiting and hard to read. A custom programming language provides the power of math, logic, and variables with utilities unique to cellular automata, like first-class support for spatial symmetry and masked N-dimensional arrays. The original drafts for NDCell used `Lua`__ for defining custom rules, but I decided to create a custom programming language for several reasons: @@ -13,11 +14,23 @@ __ https://www.lua.org/ - **Speed** — Lua can be JIT-compiled, but it still has dynamic typing which costs speed compared to a statically typed JIT-compiled language. - **Less bloat** — Though Lua is very small, it has many features that are not helpful in most cellular automata, such as tables, metatables, double-precision floating point numbers, etc. - **Guarantees** — NDCA offers useful guarantees by disallowing dynamic memory allocation and nondeterministic behavior, which improve safety and reliability. -- **Custom features** — Types like :ref:`vectors ` and :ref:`cell patterns ` are first-class in NDCA, and there is custom syntax for handling symmetries, groups of cell states, and numerous other features that would be awkward to emulate in a general-purpose programming language. +- **Custom features** — Types like vectors and cell configurations are first-class in NDCA, and there is custom syntax for handling symmetries, sets, and numerous other features that would be awkward to emulate in a general-purpose programming language. NDCA takes inspiration from the following sources: - **Rust** — basic syntax and static typing with inference - **GLSL** — first-class vectors and user code designed to run in parallel, processing a vast amount of data in small chunks - **Golly rule tables** — symmetry, ``@directive`` syntax, and pattern matching -- **Java** — `>>>` logical right-shift operator +- **Java** — ``>>>`` logical right-shift operator + +.. _set-contents-rationale: + +Why are some collections with different contents considered different types? +============================================================================ + +Values of :data:`IntSet`, :data:`VecSet`, :data:`PatternFilter`, and :data:`String` with different contents are considered different types for the purpose of variable assignment. This enables greater performance and reliability for the following reasons: + +- Operations on these types could produce arbitrarily complex values, which would require `dynamic memory allocation`__ inside the transition function, which is complex and slow and introduces the possibility of memory leaks +- Operations on these types such as iteration and pattern maching are computed at compile-time, which enables much better optimization + +__ https://en.wikipedia.org/wiki/Memory_management#HEAP diff --git a/docs/custom-rules/spec.rst b/docs/custom-rules/spec.rst index 6b9075a8..a80c7e23 100644 --- a/docs/custom-rules/spec.rst +++ b/docs/custom-rules/spec.rst @@ -12,13 +12,14 @@ __ https://en.wikipedia.org/wiki/Domain-specific_language :titlesonly: spec/types + spec/conversions spec/syntax spec/directives spec/statements spec/expressions spec/operators - spec/conversions spec/variables spec/constants spec/functions spec/methods + spec/errors diff --git a/docs/custom-rules/spec/constants.rst b/docs/custom-rules/spec/constants.rst index 006526df..69aa3f9c 100644 --- a/docs/custom-rules/spec/constants.rst +++ b/docs/custom-rules/spec/constants.rst @@ -4,26 +4,28 @@ Constants ********* +Built-in constants +================== + +These constants are automatically available in every program. + .. data:: NDIM The number of dimensions in the simulation. - NOTE: not yet implemented - + :status: Not yet implemented :type: integer .. data:: FALSE The value ``0``. - NOTE: not yet implemented - + :status: Not yet implemented :type: integer .. data:: TRUE The value ``1``. - NOTE: not yet implemented - + :status: Not yet implemented :type: integer diff --git a/docs/custom-rules/spec/conversions.rst b/docs/custom-rules/spec/conversions.rst index cafe17f4..b537feb6 100644 --- a/docs/custom-rules/spec/conversions.rst +++ b/docs/custom-rules/spec/conversions.rst @@ -1,19 +1,46 @@ +.. include:: + .. _conversions: *********** Conversions *********** -.. include:: +Some types in NDCell are implicitly converted (coerced) to other types when used with various operators or passed to functions. + +.. _subtype-coercion: + +Subtype coercion +================ + +Some types are `subtypes`__ of other types; this is implemented by coercing the subtype to the supertype when necessary. For example, a :data:`Cell` is implicitly converted to a :data:`CellSet` when used where a :data:`CellSet` is required. Here are the rules for subtype coercion: + +__ https://en.wikipedia.org/wiki/Subtyping + +- The :data:`CellSet` coerced from a :data:`Cell` contains only that cell state +- The :data:`CellSet` coerced from a :data:`Tag` contains all cell states with a :ref:`truthy ` value for that tag +- The :data:`PatternFilter` coerced from a :data:`Pattern` accepts only that pattern + +.. _boolean-conversion: + +Boolean conversion +================== + +Values of some types can be converted to a boolean, which is represented using an :data:`Int`. This can happen implicitly (when used in a place where a boolean is required) or explicitly (using :func:`bool`). -Some types in NDCell are implicitly converted to other types when used with various operators or passed to functions. +- An :data:`Int` is truthy if it is not equal to ``0``. +- A :data:`Cell` is truthy if it is not equal to ``#0``. +- A :data:`Vec` is truthy if any of its components is not equal to ``0``. +- A :data:`Pattern` is truthy if any of its cells is not equal to ``#0``. + +"Truthy" values become :data:`TRUE` (``1``) and "falsey" values (anything not truthy) become :data:`FALSE` (``0``). .. _vector-vector-conversion: Vector to vector conversion =========================== -A vectors of one length can be converted to a vector of a different length. This can happen implicitly (when used in a place where a vector of a different length is required) or explicitly (using :func:`vec`). +A :data:`Vec` of one length can be converted to a :data:`Vec` of a different length. This can happen implicitly (when used in a place where a :data:`Vec` of a different length is required) or explicitly (using :func:`vec`). - If the new length is shorter than the original length, the vector is truncated and extra components are removed. @@ -28,27 +55,8 @@ A vectors of one length can be converted to a vector of a different length. This Integer to vector conversion ============================ -An integer can be converted to a vector of any length. This can happen implicitly (when used in a place where a vector is required) or explicitly (using :func:`vec`). +An :data:`Int` can be converted to a :data:`Vec` of any length. This can happen implicitly (when used in a place where a :data:`Vec` is required) or explicitly (using :func:`vec`). A new vector is constructed with the value of the original integer for each component. - Example: ``vec3(-5)`` |rarr| ``[-5, -5, -5]`` - -.. _cell-to-cell-filter-conversion: - -Cell to cell filter conversion -=============================== - -.. _boolean-conversion: - -Boolean conversion -================== - -Any :ref:`basic type ` can be converted to a boolean, which is represented using an :ref:`integer `. This can happen implicitly (when used in a place where a boolean is required) or explicitly (using :func:`bool`). - -- An integer is truthy if it is not equal to ``0``. -- A vector is truthy if any of its components is not equal to ``0``. -- A cell is truthy if it is not equal to ``#0``. -- A pattern is truthy if any of its cells is not equal to ``#0``. - -"Truthy" values become :data:`TRUE` (``1``) and "falsey" values (anything not truthy) become :data:`FALSE` (``0``). diff --git a/docs/custom-rules/spec/directives.rst b/docs/custom-rules/spec/directives.rst index 6570e846..f5110ffd 100644 --- a/docs/custom-rules/spec/directives.rst +++ b/docs/custom-rules/spec/directives.rst @@ -13,9 +13,8 @@ Automaton directives The number of states in the automaton. - NOTE: ``@states`` will be made much more powerful in the future. - - :type: :data:`Integer` + :status: Partially implemented; more functionality planned + :type: :data:`Int` :default value: ``2`` :examples: @@ -28,7 +27,8 @@ Automaton directives __ https://en.wikipedia.org/wiki/Chebyshev_distance - :type: :data:`Integer` (nonnegative) + :status: Fully implemented + :type: :data:`Int` (nonnegative) :default value: ``1`` :examples: @@ -40,6 +40,7 @@ Automaton directives TODO: describe special variables ``this``, ``neighborhood``, and ``nbhd``. + :status: Fully implemented :type: :ref:`Code block ` :default value: ``{ remain }`` :examples: See :ref:`examples` @@ -50,9 +51,8 @@ Automaton directives __ https://www.conwaylife.com/wiki/Run_Length_Encoded - NOTE: not yet implemented, and may be removed - - :type: :ref:`String literal ` + :status: Not yet implemented; may be removed + :type: :data:`String` :default value: None :examples: @@ -65,9 +65,8 @@ Metadata directives The name of the automaton. - NOTE: not yet implemented - - :type: :ref:`String literal ` + :status: Not yet implemented + :type: :data:`String` :default value: Name of the file, excluding the extension. :examples: @@ -78,9 +77,8 @@ Metadata directives The author(s) of the NDCA file. - NOTE: not yet implemented - - :type: :ref:`String literal ` + :status: Not yet implemented + :type: :data:`String` :default value: ``"Unknown"`` :examples: @@ -92,9 +90,8 @@ Metadata directives The designer(s)/discoverer(s) of the automaton. - NOTE: not yet implemented - - :type: :ref:`String literal ` + :status: Not yet implemented + :type: :data:`String` :default value: Same as :data:`@author` :examples: @@ -106,9 +103,8 @@ Metadata directives The year that the automaton was designed/discovered. - NOTE: not yet implemented - - :type: :ref:`String literal ` + :status: Not yet implemented + :type: :data:`String` :default value: ``"Unknown"`` :examples: @@ -121,9 +117,8 @@ Metadata directives __ https://www.conwaylife.com/wiki/Main_Page - NOTE: not yet implemented - - :type: :ref:`String literal ` + :status: Not yet implemented + :type: :data:`String` :default value: ``"None"`` :examples: diff --git a/docs/custom-rules/spec/errors.rst b/docs/custom-rules/spec/errors.rst new file mode 100644 index 00000000..818413c5 --- /dev/null +++ b/docs/custom-rules/spec/errors.rst @@ -0,0 +1,9 @@ +.. _errors: + +****** +Errors +****** + +If an error occurs, the current operation is aborted and the program displays a message describing the error and where it occurred. + +To produce an error with a custom message, see :ref:`error-statement`. diff --git a/docs/custom-rules/spec/expressions.rst b/docs/custom-rules/spec/expressions.rst index fa2d3739..4456a10e 100644 --- a/docs/custom-rules/spec/expressions.rst +++ b/docs/custom-rules/spec/expressions.rst @@ -4,6 +4,10 @@ Expressions *********** +.. note:: + + This page is under construction. + TODO: talk about assignable expressions and reword all this stuff .. _assignable-expressions: diff --git a/docs/custom-rules/spec/functions.rst b/docs/custom-rules/spec/functions.rst index 8445c304..1287f89d 100644 --- a/docs/custom-rules/spec/functions.rst +++ b/docs/custom-rules/spec/functions.rst @@ -1,5 +1,3 @@ -.. _functions: - ********* Functions ********* diff --git a/docs/custom-rules/spec/methods.rst b/docs/custom-rules/spec/methods.rst index 0e49abde..51c888b4 100644 --- a/docs/custom-rules/spec/methods.rst +++ b/docs/custom-rules/spec/methods.rst @@ -1,26 +1,28 @@ -.. _methods: - ******* Methods ******* -.. _integer-methods: +.. note:: + + This page is under construction. -Integer methods +.. _int-methods: + +``Int`` methods =============== TODO -.. _vector-methods: +.. _vec-methods: -Vector methods -============== +``Vec`` methods +=============== TODO .. _cell-methods: -Cell methods -============ +``Cell`` methods +================ TODO diff --git a/docs/custom-rules/spec/operators.rst b/docs/custom-rules/spec/operators.rst index 4a7a4b33..e995783e 100644 --- a/docs/custom-rules/spec/operators.rst +++ b/docs/custom-rules/spec/operators.rst @@ -1,9 +1,13 @@ -.. _operators: +.. include:: ********* Operators ********* +.. note:: + + This page is under construction. + Besides the ``is`` operator, both `arguments`__ to any binary (two-input) operator must be of the same type, or one type must be able to implicitly convert to the other. See :ref:`conversions` for more details. Some additional rules apply for operators: __ https://en.wikipedia.org/wiki/Argument_of_a_function @@ -26,31 +30,39 @@ Arithmetic operators Given two values ``a`` and ``b``: -- ``a + b`` — addition -- ``a - b`` — subtraction -- ``a * b`` — multiplication -- ``a / b`` — division (rounds toward zero) -- ``a % b`` — remainder -- ``a ** b`` — exponentiation -- ``-a`` — negation -- ``+a`` — no operation +- ``a + b`` — Addition +- ``a - b`` — Subtraction +- ``a * b`` — Multiplication +- ``a / b`` — Division (rounds toward zero) +- ``a % b`` — Remainder +- ``a ** b`` — Exponentiation +- ``-a`` — Negation +- ``+a`` — No operation NOTE: In the future, the behavior of ``/`` and ``%`` with negative numbers may be changed to emulate `floored or euclidean division/modulo`__. __ https://en.wikipedia.org/wiki/Modulo_operation -All of these operations can be applied to integers and vectors. +All of these operations can be applied to integers and vectors. Vector operations are applied componentwise. -If an operation is applied to two vectors of different lengths or applied to a vector and an integer, then both arguments are converted to the a vector of the same length: +If an operation is applied to two vectors of different lengths, one vector is cast to the length of the other before the operation is applied. (See :ref:`vector-vector-conversion`.) -- For ``+`` and ``-``, the length of the shorter vector is used. -- +- For ``+`` and ``-``, the shorter vector is cast to the length of the **longer** one +- For ``*``, ``/``, ``%``, and ``**``, the longer vector is cast to the length of the **shorter** one -. (See [Conversions] for more details.) For ``+`` and ``-``, the shorter vector is extended; for ``*``, ``/``, ``%``, and ``**``, the longer vector is truncated. +If an operation is applied to a vector and an integer, in either order, then the integer is converted to a vector of the same length before the operation is applied. (See :ref:`integer-vector-conversion`.) -Overflow, underflow, or division by zero abort the simulation with an error. +Overflow, underflow, division by zero, or exponentiation with a negative power cause an error. (See :ref:`errors`.) -[vector lengths]: Types#vector-lengths +Examples +-------- + +- ``2 ** 8`` |rarr| ``256`` +- ``[1, 2] + [10, 20, 30]`` |rarr| ``[11, 22, 30]`` +- ``[1, 2, 3] * [1, 2]`` |rarr| ``[1, 4]`` +- ``12 / [2, 3, 4, 5]`` |rarr| ``[6, 4, 3, 2]`` +- ``[4] % [2, 0, 1]`` |rarr| ``[0]`` +- ``4 % [2, 0, 1]`` causes a division-by-zero error .. _bitwise-operators: @@ -59,24 +71,24 @@ Bitwise operators Given two values ``a`` and ``b``: -- ``a & b`` — [bitwise AND] -- ``a | b`` — [bitwise OR] -- ``a ^ b`` — [bitwise XOR] -- ``a >> b`` — [bitshift right (arithmetic/signed)][arithmetic shift] -- ``a >>> b`` — [bitshift right (logical/unsigned)][logical shift] -- ``a << b`` — [bitshift left][logical shift] -- ``~a`` — [bitwise NOT] +- ``a & b`` — `Bitwise AND `_ +- ``a | b`` — `Bitwise OR `_ +- ``a ^ b`` — `Bitwise XOR `_ +- ``a >> b`` — `Bitshift right (arithmetic/signed) `_ +- ``a >>> b`` — `Bitshift right (logical/unsigned) `_ +- ``a << b`` — `Bitshift left `_ +- ``~a`` — `Bitwise NOT `_ + +All of these operations can be applied to integers and vectors. Vector operations are applied componentwise. -[bitwise AND]: https://en.wikipedia.org/wiki/Bitwise_operation#AND -[bitwise OR]: https://en.wikipedia.org/wiki/Bitwise_operation#OR -[bitwise XOR]: https://en.wikipedia.org/wiki/Bitwise_operation#XOR -[arithmetic shift]: https://en.wikipedia.org/wiki/Arithmetic_shift -[logical shift]: https://en.wikipedia.org/wiki/Logical_shift -[bitwise NOT]: https://en.wikipedia.org/wiki/Bitwise_operation#NOT +If an operation is applied to two vectors of different lengths, one vector is cast to the length of the other before the operation is applied. (See :ref:`vector-vector-conversion`.) -All of these operations are defined for integers and vectors. +- For ``|``, ``^``, ``>>``, ``<<<``, and ``<<``, the shorter vector is cast to the length of the **longer** one +- For ``&``, the longer vector is cast to the length of the **shorter** one -Bitshifting by less than 0 or more than 64 aborts the simulation with an error. +If an operation is applied to a vector and an integer (in either order), then the integer is converted to a vector of the same length. (See :ref:`integer-vector-conversion`.) + +Bitshifting by less than 0 or more than 64 causes an error. (See :ref:`errors`.) .. _set-operators: @@ -85,23 +97,20 @@ Set operators Given two values ``a`` and ``b``: -- ``a & b`` — [intersection] -- ``a | b`` — [union] -- ``a ^ b`` — [symmetric difference] -- ``~a`` — [complement] - -[intersection]: https://en.wikipedia.org/wiki/Intersection_(set_theory) -[union]: https://en.wikipedia.org/wiki/Union_(set_theory) -[symmetric difference]: https://en.wikipedia.org/wiki/Symmetric_difference -[complement]: https://en.wikipedia.org/wiki/Complement_(set_theory) +- ``a & b`` — `Intersection `_ +- ``a | b`` — `Union `_ +- ``a ^ b`` — `Symmetric difference `_ +- ``a &~ b`` — `Relative complement of B in A `_ -Set operations are defined for cell filters. If one of these operators is applied to a cell, it is automatically converted to a cell filter that matches only that cell. +All of these operations can be applied to any :ref:`set/filter type ` or its subtypes. .. _comparison-operators: Comparison operators ==================== +Given two values ``a`` and ``b``: + - ``a == b`` — Does ``a`` equal ``b``? - ``a != b`` — Does ``a`` not equal ``b``? - ``a < b`` — Is ``a`` less than ``b``? @@ -109,22 +118,29 @@ Comparison operators - ``a <= b`` — Is ``a`` less than or equal to ``b``? - ``a >= b`` — Is ``a`` greater than or equal to ``b``? -All of these operations are defined for integers and vectors. ``==`` and ``!=`` are defined for cell states and patterns. +All of these comparisons can be applied to integers and vectors. ``==`` and ``!=`` can be applied to cell states and patterns. Vector comparisons are applied componentwise. + +If a comparison is applied to two vectors of different lengths, the shorter vector is to the length of the longer one before the operation is applied. (See :ref:`vector-vector-conversion`.) + +If a comparison is applied to a vector and an integer (in either order), then the integer is converted to a vector of the same length. (See :ref:`integer-vector-conversion`.) + +A ``!=`` comparison between two vectors results in :data:`TRUE` if **any** corresponding components of the two vectors are unequal. All other comparisons between two vectors result in :data:`TRUE` if only if the comparison is :data:`TRUE` for **all** corresponding components of the two vectors. Examples: + +- ``[1, 2] == [1, 2, 0]`` |rarr| :data:`TRUE` +- ``[1, 2] == [1, 2, 3]`` |rarr| :data:`FALSE` because ``0 == 3`` |rarr| :data:`FALSE` +- ``[1, 2] != [1, 2, 3]`` |rarr| :data:`TRUE` because ``0 != 3`` |rarr| :data:`TRUE` +- ``[-1, 2] < [0, 4]`` |rarr| :data:`TRUE` because ``-1 < 0`` |rarr| :data:`TRUE` and ``2 < 4`` |rarr| :data:`TRUE` +- ``[-1, 2] < [0, 1]`` |rarr| :data:`FALSE` because ``2 < 1`` |rarr| :data:`FALSE` .. _boolean-operators: Boolean operators ================= -- ``a and b`` — [logical AND] -- ``a or b`` — [logical OR] -- ``a xor b`` — [logical XOR] -- ``not a`` — [logical NOT] - -[logical AND]: https://en.wikipedia.org/wiki/AND_gate -[logical OR]: https://en.wikipedia.org/wiki/OR_gate -[logical XOR]: https://en.wikipedia.org/wiki/XOR_gate -[logical NOT]: https://en.wikipedia.org/wiki/Inverter_(logic_gate) +- ``a and b`` — `Logical AND `_ +- ``a or b`` — `Logical OR `_ +- ``a xor b`` — `Logical XOR `_ +- ``not a`` — `Logical NOT `_ .. _range-operator: @@ -133,18 +149,20 @@ Range operator - ``a..b`` -.. _indexing: +.. _vector-indexing: + +Vector indexing +=============== -Indexing -======== +- ``v[n]`` -- ``a[b]`` +Indexing a vector ``v`` by an integer ``n`` results in the ``n``-th component of ``v``. The X component has index ``0``, the Y component has index ``1``, etc. Indexing with a value ``n`` that is not between ``0`` and ``v.len - 1`` (inclusive) causes an error. (See :ref:`errors`.) .. _is-operator: -Membership test -=============== +Filter test +=========== - ``a is b`` -This operator takes a basic type for ``a`` and the corresponding filter type for ``b``. (Note that implicit type conversion rules apply) +This operator takes a basic type for ``a`` and the corresponding filter type for ``b``. diff --git a/docs/custom-rules/spec/statements.rst b/docs/custom-rules/spec/statements.rst index 23a38aa3..35dfb529 100644 --- a/docs/custom-rules/spec/statements.rst +++ b/docs/custom-rules/spec/statements.rst @@ -4,6 +4,10 @@ Statements ********** +.. note:: + + This page is under construction. + Several statements involve boolean conversion; for more details, see :ref:`boolean-conversion`. .. contents:: @@ -177,7 +181,7 @@ Debugging Error statement --------------- -An error statement consists of the keyword ``error``, optionally followed by a :ref:`string literal ` specifying a custom error message. An error statement causes an error, which aborts the simulation. +An error statement consists of the keyword ``error``, optionally followed by a :data:`String` specifying a custom error message. An error statement causes an error, which aborts the simulation. Examples: @@ -191,7 +195,7 @@ Examples: Assert statement ---------------- -An assert statement consists of the keyword ``assert``, followed by an :ref:`expression `, and then an optional comma and :ref:`string literal ` specifying a custom error message. The expression must be able to be converted to a boolean. An assert statement evaluates the expression and if the result is falsey it causes an error, which aborts the simulation. +An assert statement consists of the keyword ``assert``, followed by an :ref:`expression `, and then an optional comma and :data:`String` specifying a custom error message. The expression must be able to be converted to a boolean. An assert statement evaluates the expression and if the result is falsey it causes an error, which aborts the simulation. Examples: diff --git a/docs/custom-rules/spec/syntax.rst b/docs/custom-rules/spec/syntax.rst index faca12f1..74864f09 100644 --- a/docs/custom-rules/spec/syntax.rst +++ b/docs/custom-rules/spec/syntax.rst @@ -1,9 +1,11 @@ -.. _syntax: - ****** Syntax ****** +.. note:: + + This page is under construction. + Like Lua and most C-family programming languages: - :ref:`Code blocks ` begin with ``{`` and end with ``}`` @@ -29,6 +31,7 @@ The file is split into tokens, where each token is one of the following: - String beginning and ending with ``'`` (may contain any character except ``'``) - Number with a decimal point, matching the `regex`_ ``-?\d?\.\d+`` (currently unused) - Number without a decimal point, matching the `regex`_ ``-?\d+`` +- Keyword (see :ref:`keywords`) - Identifier, matching the `regex`_ ``[A-Za-z_][A-Za-z_\d]*`` - Tag name, consisting of ``#`` followed immediately by an identifier (no space) - Directive name, consisting of ``@`` followed immediately by an identifier (no space) @@ -50,6 +53,46 @@ NOTE: string syntax may change in the future NOTE: either document and use string prefix characters, or remove support for them from the lexer +.. _keywords: + +Keywords +======== + +The following keywords are reserved, and cannot be used for identifiers: + +- ``and`` +- ``assert`` +- ``become`` +- ``bind`` +- ``bound`` +- ``break`` +- ``case`` +- ``colors`` +- ``continue`` +- ``else`` +- ``error`` +- ``for`` +- ``icons`` +- ``if`` +- ``in`` +- ``is`` +- ``match`` +- ``models`` +- ``not`` +- ``or`` +- ``remain`` +- ``return`` +- ``same`` +- ``static`` +- ``transition`` +- ``unless`` +- ``where`` +- ``while`` +- ``with`` +- ``xor`` + +Some of these are currently used, and some are reserved for future use. + .. _file-syntax: File structure diff --git a/docs/custom-rules/spec/types.rst b/docs/custom-rules/spec/types.rst index 0f93a6b2..ab757c2e 100644 --- a/docs/custom-rules/spec/types.rst +++ b/docs/custom-rules/spec/types.rst @@ -4,128 +4,162 @@ Types ***** -Note that all types except :ref:`patterns ` have `value semantics`__, which means that modifying a value in one variable does not have an effect on any other variables. +Overview +======== -__ https://en.wikipedia.org/wiki/Value_semantics +- There are two important primitive types: :data:`Int` and :data:`Cell` +- Each of these two primitive types has a collection type: :data:`Vec` (collection of :data:`Int`) and :data:`Pattern` (collection of :data:`Cell`) +- Each of these four types has a corresponding set/filter type: :data:`IntSet`, :data:`CellSet`, :data:`VecSet`, and :data:`PatternFilter` -.. _basic-types: + - :data:`Cell` is a subtype [#f1]_ of :data:`CellSet` + - :data:`Pattern` is a subtype of :data:`PatternFilter` -Basic types -=========== +- There are two other primitive types: :data:`Tag` and :data:`String` -.. _integer: + - :data:`Tag` is also a subtype of :data:`CellSet` -.. data:: Integer +.. [#f1] I.e. anywhere that a :data:`CellSet` is required, a :data:`Cell` is accepted as well. All operations on a :data:`CellSet` are also allowed on a :data:`Cell`. See https://en.wikipedia.org/wiki/Subtyping. - :aliases: ``Int`` - :methods: :ref:`integer-methods` +See :ref:`subtype-coercion` for more about subtypes. - Integers are represented using 64-bit signed two's complement. This means the minimum value is ``-9223372036854775808`` and the maximum value is ``9223372036854775807``. +See :ref:`variable-types` regarding how variables use the type system. - Integers are also used for boolean values; ``0`` is "falsey" and any other number (generally ``1``) is "truthy." +.. _primitive-types: - Integers are written as a sequence of digits without a leading zero but with an optional ``+`` or ``-`` at the beginning. Examples: ``0``, ``-1``, ``42``, ``+6``, ``-32768``. +Primitive types +=============== - NOTE: In the future, hexadecimal and/or binary literals may be supported. +.. data:: Int -.. _vector: + :status: Fully implemented + :methods: :ref:`int-methods` + :operators: :ref:`arithmetic-operators`, :ref:`bitwise-operators`, :ref:`comparison-operators` -.. data:: Vector + An integer, represented using a 64-bit signed two's complement integer. This means the minimum value is ``-9223372036854775808`` and the maximum value is ``9223372036854775807``. - :ref:`vector-methods` + Boolean values are represented using integers. (See :ref:`boolean-conversion`.) - A vector is a sequence of :ref:`integers ` of a fixed length. Each integer is a component of that vector, and the number of components is the length of that vector. Vectors of different lengths are different types. The length of a vector a must be between 1 and 256 (inclusive). + An integer literal consists of a sequence of digits without a leading zero but with an optional ``+`` or ``-`` at the beginning. Examples: - The first component of a vector is the X component at index 0; the second is the Y component at index 1; etc. + - ``0`` + - ``-1`` + - ``42`` + - ``+6`` + - ``-32768`` - Vectors are written as a list of integers separated by commas surrounded by square brackets. For example, ``[3, -1, 0]`` is a vector of length ``3`` with X component ``3``, Y component ``-1``, Z component ``0``. Vectors can also be written using :func:`vec()` and its variants. +.. data:: Cell - Example vector types: + :status: Fully implemented + :methods: :ref:`cell-methods` + :operators: :ref:`set-operators`, :ref:`comparison-operators` (``==`` and ``!=`` only) + :subtype of: :data:`CellSet` - - ``Vector1`` - - ``Vector3`` - - ``Vector256`` + A cell state, represented using an 8-bit unsigned integer. This means the minimum value is ``0`` and the maximum value is ``255``, so an automaton cannot have more than 256 states. :data:`Cell` values are always within the range of valid cell states in a cellular automaton. For example, an automaton with 10 states has a maximum cell state ID of ``9``. -### Vector arithmetic + :data:`Cell` is a subtype of :data:`CellSet`. When used in place of a :data:`CellSet`, a :data:`Cell` represents a set containing only the one cell state. -Vectors support all the same arithmetic and bitwise operations as [integers][integer] by applying them componentwise. For example, `[1, 2, 3] + [10, 20, 30]` results in `[11, 22, 33]`. + A :data:`Cell` literal consists of the ``#`` operator followed by the cell state ID. Examples: -For most operations, when an operation is applied between vectors of different lengths, the shorter vector is first extended using `0`. For example, `[1, 2] + [10, 20, 30]` results in `[11, 22, 30]`. For multiplication (`*`) and bitwise AND (`&`), however, the longer vector is truncated to the length of the shorter one, since the extra components would be zero anyway. So `[1, 2, 3] * [1, 2]` results in `[1, 4]`, **not** `[1, 4, 0]`. + - ``#0`` + - ``#1`` + - ``#42`` -### Vector comparisons + A :data:`Cell` literal may use an arbitrary integer expression for the cell state ID by surrounding the expression in parentheses. Examples: -Vectors support all the same comparisons as [integers][integer], by applying them componentwise. When comparing vectors, the shorter vector is first extended using `0`. A comparison between vectors compares all components, and is true only if that comparison is true for all components. For example `[-1, 2] < [0, 4]` is true because `-1 < 0` and `2 < 4` are both true. `[-1, 2] < [0, 1]`, however, is false because `2 < 1` is false. + - ``#(my_variable)`` + - ``#(x + 5)`` -.. _cell: +.. data:: Tag -.. data:: Cell + :status: Not yet implemented - Cells are represented using unsigned 8-bit integers holding ID of the cell's state. This means the minimum value is ``0`` and the maximum value is ``255``, so an automaton cannot have more than 256 states. Cells values are always within the range of valid cell states in a cellular automaton. For example, an automaton with ``10`` states has a maximum cell state ID of ``9``. + This type's design is still a work in progress. - Single cell states are written using the ``#`` operator followed by a number. Examples: ``#0``, ``#1``, ``#42``. To use the value of a variable or expression instead of a literal integer, surround the expression in parentheses: ``#(my_variable)`` or ``#(10 + 5)``. +.. data:: String -### Cell operations + :status: Partially implemented -Cells are automatically converted to [cell filters][cell filter] when used with any set operator + Different :data:`String` values are different types, and therefore cannot be stored in the same variable. (See :ref:`set-contents-rationale`) -### Cell comparisons + This type's design is still a work in progress. -Cells support the comparison operators ``==`` and ``!=``, which compare the IDs. +.. _collection-types: -.. _pattern: +Collection types +================ -.. data:: Pattern +.. data:: Vec - TODO + :status: Fully implemented + :methods: :ref:`vec-methods` + :operators: :ref:`arithmetic-operators`, :ref:`bitwise-operators`, :ref:`comparison-operators`, :ref:`vector-indexing` -.. _filter-types: + A vector, represented using a fixed-length array of :data:`Int` values. Each :data:`Int` value is a component of the :data:`Vec`, and the number of components is the length of the :data:`Vec`. The length of a :data:`Vec` must be between 1 and 256 (inclusive). :data:`Vec` values of different lengths are different types, and therefore cannot be stored in the same variable. -Filter types -============ + The first component of a :data:`Vec` is the X component at index 0; the second is the Y component at index 1; etc. -.. _range: + A :data:`Vec` literal consists of a list of integer expressions separated by commas surrounded by square brackets. Examples: -.. data:: Range + - ``[3, -1, 0]`` is a :data:`Vec` of length ``3`` with X component ``3``, Y component ``-1``, Z component ``0`` + - ``[6]`` is a :data:`Vec` of length ``1`` with X component ``6`` + - ``[a, b]`` is a :data:`Vec` of length ``2`` with X compoment ``a`` and Y component ``b``, given ``a`` and ``b`` are integers - TODO + A :data:`Vec` literal may contain other vectors, which are concatenated to produce the result. Examples: -.. _rectangle: + - ``[v1, -3, v2]`` is a :data:`Vec` constructed by concatenating ``v1``, ``[-3]``, and ``v2`` -.. data:: Rectangle + A :data:`Vec` can also be constructed using :func:`vec()` and its variants. - TODO +.. data:: Pattern -.. _cell-filter: + :status: Partially implemented -.. data:: Cell filter + A configuration of cells. Patterns with different shapes are different types. - TODO +.. _filter-types: -.. _pattern-filter: +Set/filter types +================ -.. data:: Pattern filter +.. data:: IntSet - TODO + :status: Implementation in progress + :operators: :ref:`set-operators` -.. _other-types: + A finite set of :data:`Int`. Different :data:`IntSet` values are different types, and therefore cannot be stored in the same variable. (See :ref:`set-contents-rationale`) -Other types -=========== + An :data:`IntSet` literal consists of a comma-separated list of :data:`Int` or :data:`IntSet` surrounded by curly braces. Examples: -.. _tag: + - ``{}`` constructs the empty set, containing no integers + - ``{42}`` constructs a set containing only the integer 42 + - ``{1, 2, 3, 4}`` constructs a set containing the integers 1, 2, 3, and 4 + - ``{1, 2, 3, 4,}`` is also allowed (but discouraged unless spanning multiple lines) -.. data:: Tag + An :data:`IntSet` can also be constructed using a range literal consisting of two integers separated by ``..``. Examples: - TODO + - ``1..5`` is equivalent to ``{1, 2, 3, 4, 5}`` + - ``-3..+3`` contains all integers from -3 to 3 (inclusive) + - ``{-4..-1, 1..99}`` contains all integers from -4 to 99 (inclusive) *except* 0 -.. _string: +.. data:: VecSet -.. data:: String + :status: Implementation in progress + :operators: :ref:`set-operators` + + A finite set of :data:`Vec`, all with the same length. Different :data:`VecSet` values are different types, and therefore cannot be stored in the same variable. (See :ref:`set-contents-rationale`) + +.. data:: CellSet + + :status: Partially implemented + + A set of cell states. Unlike :data:`IntSet` and :data:`VecSet`, all :data:`CellSet` values are the same type. + + This type's design is still a work in progress. - Strings cannot be stored in variables. +.. data:: PatternFilter -.. _void: + :status: Not yet implemented -.. data:: Void + Different :data:`PatternFilter` values are different types, and therefore cannot be stored in the same variable. (See :ref:`set-contents-rationale`) - The void type is an implementation detail that will probably be removed in a future version. Ignore it for now. + This type's design is still a work in progress. diff --git a/docs/custom-rules/spec/variables.rst b/docs/custom-rules/spec/variables.rst index 64a6abe2..76c21cec 100644 --- a/docs/custom-rules/spec/variables.rst +++ b/docs/custom-rules/spec/variables.rst @@ -4,7 +4,18 @@ Variables ********* -NDCA is statically typed, meaning that a given variable can only store one type of value, however all variable types are inferred. +.. note:: + + This page is under construction. + +.. _variable-types: + +Variable typing +=============== + +NDCA is `statically typed`__, which means that each variable has one :ref:`type ` of value it can store. Variable types are automatically inferred based on the values assigned to them. + +__ https://en.wikipedia.org/wiki/Type_system#Static_type_checking .. _variable-names: @@ -17,3 +28,33 @@ TODO: list reserved words here Variable assignment =================== + +Semantics +========= + +Variables use `value semantics`__, which means that modifying a value in one variable does not have an effect on any other variables. + +__ https://en.wikipedia.org/wiki/Value_semantics + +Built-in variables +================== + +These variables are automatically available in every program. + +.. data:: this + + The state of the current cell being updated. This variable is immutable. + + :type: :data:`Cell` + +.. data:: nbhd + + Alias for :data:`neighborhood`. + + :type: :data:`Pattern` + +.. data:: neighborhood + + The pattern of cells surrounding the current cell. This variable is immutable. + + :type: :data:`Pattern` diff --git a/docs/formats.rst b/docs/formats.rst index 9ce00f68..930f06ae 100644 --- a/docs/formats.rst +++ b/docs/formats.rst @@ -29,7 +29,7 @@ Conventionally Y coordinates increase downwards in an RLE; in NDCell, however, Y N-dimensional Macrocell format (NDMC) ===================================== -For interchanging large patterns, NDCell uses a Macrocell format that is mostly backwards-compatible with Golly's `Macrocell format`__. While Golly has a variant for some two-state algorithms, NDCell always uses the generic format that supports any number of states (though it is able to parse the two-state variant). It introduces the following new features: +For interchanging large patterns, NDCell uses a Macrocell format that is mostly backwards-compatible with Golly's `Macrocell format`__. While Golly has a variant for some two-state algorithms, NDCell always exports using the generic format that supports any number of states (though it is able to import the two-state variant). It introduces the following new features: __ http://golly.sourceforge.net/Help/formats.html#mc diff --git a/docs/index.rst b/docs/index.rst index a6c946cd..f604b4ef 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,12 +7,23 @@ NDCell is an N-dimensional `cellular automaton`__ simulator written in Rust. You __ https://en.wikipedia.org/wiki/Cellular_automaton __ https://www.github.com/HactarCE/NDCell +.. + TODO add these to root toctree once they are ready: + Getting started + custom-rules/examples + also figure out why examples are not rendering? + .. toctree:: :maxdepth: 2 :caption: Custom rules :hidden: - Getting started - custom-rules/examples Specification custom-rules/rationale + +.. toctree:: + :maxdepth: 2 + :caption: Other + :hidden: + + formats diff --git a/examples/ed-rep.ndca b/examples/ed-rep.ndca new file mode 100644 index 00000000..8858d829 --- /dev/null +++ b/examples/ed-rep.ndca @@ -0,0 +1,5 @@ +@transition { + ret = (this == #1) + for cell in nbhd { ret += (cell == #1) } + become #(ret & 7) +} diff --git a/examples/ed-rep.rle b/examples/ed-rep.rle new file mode 100644 index 00000000..232b44ea --- /dev/null +++ b/examples/ed-rep.rle @@ -0,0 +1,24 @@ +x = 49, y = 49, rule = Ed-rep +ABA2B2ADE17F4E11D4B4A$2A2B2AB23FE10D2B3AB2A$4B2AD24F3E7DB4AB2A$3B3AE +24F3E7DB7A$3B2AB25F7E3DCB6A$3B2AC3FE17F6E2D2BC3DCB4ABA$3B2AC3FE19F4D +5B2DED2B3A2B$2B3AD12FE9FD3B4A3BD2EDB4AB$2B2ABE2F3E7FE5F3EDBA2BA4B3CDE +D3B2AB$BC2ABD3FE7F2E5F3DB2D2BC5D2C2DC3B3A$2BA2BD10FE2D3F2E6DC10D3C3B +2A$2BA2BC7F6DEF3E5DC3D2E7DBCD3BA$2BA2BC3F2DC3BC3DEFE5DC3DE2FE4D2EDBCD +2CBA$BCA3BEFDB3AB3D5E3D3BC3DB7AB2DCD2C2B$BCA3BDFD2B2C7D4ED4BDB6A4BC5D +CB$BCB2ABCF3D2E7DE3FD5B3AB2C11D2B$B2C3ABF8E5D3FE4B2DCDEFE6DE4D2B$ABCB +3A2FDF2E3FE2D2B3FD3BD2E3D2E2DB2D3E2DEDB$A3B3AEF3E2FDBA3BD3FEC8BC5D4E +4DB$2ABCB2AEF2EFC6ABE3FD2B10D3E7DEC$CABC2BAEF2E4A2B2CE4FE4D3E2FE4FEDE +3D3ED$DABC2BADFEDAD3FEB2D4FE2DBD3EFEF7E3D4E$BA2BCBAD2FBD3FD2B2D4F2E5D +EF6E6DE2DE$6BAC2FD2E2DBC2E5F2DED3BD3F4E5D2E2DE$D3BC2BD2F2E4DE7F3EFC2A +BD2F4E5D3EDE$DA2BC2BD3F5E8FE2DEF2B2A13DEDE$C3B2DCB11FD5F3DF2DB2AB13DE +$2C2BCEDC10F2D5F3D2F6B11D2E$C2BCB2E2D9FBD4F3DB2D6B9DC3D$2B3C2E2C8FDB +2FE2FE2DB2DBD4B9DC3D$B2C2B2EDBE6FDBA2FE2F4DB3A4BC8D2C2D$3BABC2EDE4F2E +DAB5F3DB3A7BD2BC7D$A3B2AEF3E2FE3DAB8DBA6BC2B2DBDE6D$3B3AB7E2DBA3DBABD +2BA6BDC2B2CBCE3DB2D$B3AB2C7E2D2BFD2E2DC6BC2DCD4BCDE6D$CBC2BECBCD5EDBD +F2D2E2D3C8DB2AB9D$ECECBC3ACDF3EDBD6EDC7D3B3AB9D$CB4C2A3CF2E2DA2D2E2D +2F3ED4B5AB2C5DC2D$2B2CECBCECBEFEDCB5EFE2D2BABDB5ACD3C4DC2D$2B4CB2CBAE +FE2DBDED2FE4BDB6ABC8DCB2D$10BABFE7D3BC2DB4ABCB3C6D2CB2D$2B2A8B4E4DB8A +CD4BDBC2DC4DBC2D$B2AC5BCD2EF3E2DBDECBA3BC5D3B8DBC2D$BABC5BCE2CFEDE2DB +D2FEDED3F2DBC2BCD2C5DBC2D$C2BC3BC2BCAB2ED2E3DE2F2E5D4CB3C3DCDCBC2D$EB +A2B6C2BD5E2D6E2DB2CDC3BC6D3B2C$B4A4B5C6ED5E8DC9DC2B2C$2AB2A3B3C9ED3E +10DB9DC2B2C$2B3CE2CE4C2BDF3ED4E19DC2B2C! diff --git a/examples/ed-rep_colors.txt b/examples/ed-rep_colors.txt new file mode 100644 index 00000000..84685007 --- /dev/null +++ b/examples/ed-rep_colors.txt @@ -0,0 +1,6 @@ +rgb(56, 32, 16) +rgb(104, 80, 56) +rgb(120, 120, 72) +rgb(168, 120, 104) +rgb(200, 168, 120) +rgb(216, 176, 168) diff --git a/examples/empty.rle b/examples/empty.rle new file mode 100644 index 00000000..bfbf89b2 --- /dev/null +++ b/examples/empty.rle @@ -0,0 +1 @@ +x=0,y=0 diff --git a/examples/wireworld_2d.ndca b/examples/wireworld_2d.ndca new file mode 100644 index 00000000..ee09c4b2 --- /dev/null +++ b/examples/wireworld_2d.ndca @@ -0,0 +1,11 @@ +@states 4 + +@transition { + if this is #3 { + sum = 0 + for cell in nbhd { sum += (cell == #1) } + if 1 <= sum <= 2 { become #1 } + } + if this is #1 { become #2 } + if this is #2 { become #3 } +} diff --git a/examples/wireworld_3d.ndca b/examples/wireworld_3d.ndca new file mode 100644 index 00000000..e1b2f366 --- /dev/null +++ b/examples/wireworld_3d.ndca @@ -0,0 +1,12 @@ +@states 4 +@ndim 3 + +@transition { + if this is #3 { + sum = 0 + for cell in nbhd { sum += (cell == #1) } + if 1 <= sum <= 2 { become #1 } + } + if this is #1 { become #2 } + if this is #2 { become #3 } +} diff --git a/examples/wireworld_colors.txt b/examples/wireworld_colors.txt new file mode 100644 index 00000000..2fd6924b --- /dev/null +++ b/examples/wireworld_colors.txt @@ -0,0 +1,3 @@ +#0080FF +#fff +#FF8000 diff --git a/examples/wireworld_xor_3d.rle b/examples/wireworld_xor_3d.rle new file mode 100644 index 00000000..8eb0fcda --- /dev/null +++ b/examples/wireworld_xor_3d.rle @@ -0,0 +1,7 @@ +#CXRLE Pos=-23,-2,-15 +x = 30, y = 3, z = 53 +2$5.23C/2$28.C/$24.C$29.C/23.4C2$10CBA4CBA6C5.C/23.C2.C$27.C$28.C/23. +4C2$4CBA10CBA6C/$24.C14/2$5.23C/2$28.C/2$13CBACBA5C6.C/$23.C$23.C5.C/$ +23.C$23.6C/$23.C$23.C/2$10CBA4CBA5C18/2$20.7C/2$5.15C7.C/2$22.2C4.C/2$ +10CBA4CBA4C2.C4.C/2$23.4C2.C/2$23.C2.3C/2$23.4C/2$4CBA10CBA4C2.C/2$22. +2C! diff --git a/ui/Cargo.toml b/ui/Cargo.toml index 2e3869ae..5636ba0a 100644 --- a/ui/Cargo.toml +++ b/ui/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ndcell" -version = "0.2.0-dev" +version = "0.2.0" authors = ["HactarCE <6060305+HactarCE@users.noreply.github.com>"] edition = "2018" @@ -11,19 +11,23 @@ anyhow = "1.0" cgmath = "0.18" colorous = "1.0" clipboard = "0.5" +css-color-parser = "0.1.2" enum_dispatch = "0.3" glium = "0.29" itertools = "0.10" -imgui = "0.6" -imgui-glium-renderer = "0.6" -imgui-winit-support = "0.6" +imgui = "0.7" +imgui-glium-renderer = "0.7" +imgui-winit-support = "0.7" lazy_static = "1.4" log = "0.4" mimalloc = { version = "*", default-features = false } +nfd2 = "0.2" +palette = "0.5" parking_lot = "0.11" # preferences = "1.1" send_wrapper = "0.5" simple_logger = "1.11" +sloth = "0.2" ndcell_core = { path = "../core" } ndcell_lang = { path = "../lang" } diff --git a/ui/src/colors.rs b/ui/src/colors.rs index e65e07c0..abe1c7ff 100644 --- a/ui/src/colors.rs +++ b/ui/src/colors.rs @@ -1,29 +1,90 @@ -/// Color of the gridlines. This will be configurable in the future. -pub const GRIDLINES: [f32; 4] = [0.25, 0.25, 0.25, 1.0]; +//! Color constants. All of these will be configurable in the future. -/// Color given to the hovered cell when drawing. This will be configurable in -/// the future. -pub const HOVERED_DRAW: [f32; 4] = [0.0, 0.5, 1.0, 1.0]; +use palette::{Srgb, Srgba}; -/// Color given to the hovered cell when selecting. This will be configurable in -/// the future. -pub const HOVERED_SELECT: [f32; 4] = [1.0, 1.0, 1.0, 1.0]; +/// Defines an `Rgb` value at compile time. +macro_rules! rgb { + ($r:expr, $g:expr, $b:expr) => { + palette::rgb::Rgb { + red: $r, + green: $g, + blue: $b, + standard: std::marker::PhantomData, + } + }; +} +/// Defines an `Rgba` value at compile time. +macro_rules! rgba { + ($r:expr, $g:expr, $b:expr, $a:expr) => { + palette::Alpha { + color: rgb!($r, $g, $b), + alpha: $a, + } + }; +} -/// Color given to the selection resize preview. This will be configurable in -/// the future. -pub const SELECTION_RESIZE: [f32; 4] = [0.0, 0.5, 1.0, 0.4]; +/// 2D background color. +pub const BACKGROUND_2D: Srgb = rgb!(0.0, 0.0, 0.0); +/// 3D background color. +pub const BACKGROUND_3D: Srgb = rgb!(0.0, 0.0, 0.0); -/// Color given to the selection. This will be configurable in the future. -pub const SELECTION: [f32; 4] = [0.6, 0.6, 0.6, 0.8]; +/// Color of the gridlines. +pub const GRIDLINES: Srgba = rgba!(0.25, 0.25, 0.25, 1.0); -/// 2D cell background color. This will be configurable in the future. -pub const BACKGROUND_2D: (f32, f32, f32, f32) = (0.0, 0.0, 0.0, 1.0); +/// Crosshair opacity change. +pub const CROSSHAIR_OPACITY: f32 = 0.2; -/// 3D cell background color. This will be configurable in the future. -pub const BACKGROUND_3D: (f32, f32, f32, f32) = (0.0, 0.0, 0.0, 1.0); +pub mod hover { + use super::*; -/// Color for dead cells. This will be configurable in the future. -pub const DEAD: [u8; 4] = [0, 0, 0, 0]; + /// Color of the hovered cell when drawing in `PLACE` mode. + pub const PLACE_FILL: Srgba = rgba!(0.2, 0.5, 0.2, 0.75); + /// Color of the outline around the hovered cell when drawing in `PLACE` + /// mode. + pub const PLACE_OUTLINE: Srgb = rgb!(0.4, 1.0, 0.4); -/// Color for live cells. This will be configurable in the future. -pub const LIVE: [u8; 4] = [255, 255, 255, 255]; + /// Color of the hovered cell when drawing in `REPLACE` mode. + pub const REPLACE_FILL: Srgba = rgba!(0.0, 0.2, 0.5, 0.75); + /// Color of the outline around the hovered cell when drawing in `REPLACE` + /// mode. + pub const REPLACE_OUTLINE: Srgb = rgb!(0.0, 0.4, 1.0); + + /// Color of the hovered cell when drawing in `ERASE` mode. + pub const ERASE_FILL: Srgba = rgba!(0.4, 0.1, 0.1, 0.75); + /// Color of the outline around the hovered cell when drawing in `ERASE` + /// mode. + pub const ERASE_OUTLINE: Srgb = rgb!(0.8, 0.2, 0.2); + + /// Color of the hovered cell when selecting. + pub const SELECT_FILL: Srgba = rgba!(0.6, 0.6, 0.6, 0.75); + /// Color of the outline around the hovered cell when selecting. + pub const SELECT_OUTLINE: Srgb = rgb!(0.8, 0.8, 0.8); +} + +pub mod selection { + use super::*; + + /// Color given to the region selection. + pub const REGION_FILL: Srgba = rgba!(0.4, 0.6, 0.8, 0.25); + /// Color of the outline around the region selection. + pub const REGION_OUTLINE: Srgb = rgb!(0.4, 0.6, 0.8); + + /// Color of the cell selection. + pub const CELLS_FILL: Srgba = rgba!(0.4, 0.6, 0.8, 0.0); + /// Color of the outline around the cell selection. + pub const CELLS_OUTLINE: Srgb = rgb!(0.4, 0.6, 0.8); + + /// Color of the selection resize preview. + pub const RESIZE_FILL: Srgba = rgba!(0.8, 0.4, 0.0, 0.125); + /// Color of the outline around the selection resize preview. + pub const RESIZE_OUTLINE: Srgb = rgb!(0.8, 0.4, 0.0); +} + +pub mod cells { + use super::*; + + /// Color for dead cells. + pub const DEAD: Srgba = rgba!(0.0, 0.0, 0.0, 0.0); + /// Color for live cells. + pub const LIVE: Srgba = rgba!(1.0, 1.0, 1.0, 1.0); +} diff --git a/ui/src/commands.rs b/ui/src/commands.rs index ff0889b8..8cadedf6 100644 --- a/ui/src/commands.rs +++ b/ui/src/commands.rs @@ -1,134 +1,399 @@ use cgmath::Deg; +use log::error; +use palette::{Srgb, Srgba}; use ndcell_core::prelude::*; -use crate::Scale; +use crate::mouse::MouseDisplayMode; +use crate::{Direction, Face}; -macro_rules! impl_command_from { - ( Command::$command_variant:ident($inner:ty) ) => { - impl From<$inner> for Command { - fn from(c: $inner) -> Self { - Self::$command_variant(c) - } - } - }; +/// Message sent to a `GridView` to enqueue a command. +#[derive(Debug, Clone)] +pub struct CmdMsg { + pub command: Cmd, + pub cursor_pos: Option, +} +impl> From for CmdMsg { + fn from(command: T) -> Self { + command.into().to_msg() + } } +/// Command issued to a `GridView`. #[derive(Debug, Clone)] -pub enum Command { - Sim(SimCommand), - History(HistoryCommand), - View(ViewCommand), - Draw(DrawCommand), - Select(SelectCommand), - GarbageCollect, - - ContinueDrag(FVec2D), - StopDrag, +pub enum Cmd { + /// Starts a drag command. + BeginDrag(DragCmd), + /// Continues a drag command with a new mousue cursor position. (hidden) + ContinueDrag, + /// Ends a drag command. (hidden) + EndDrag, + /// Cancels a drag command. + CancelDrag, + /// Cancels any operation. Cancel, -} -#[derive(Debug, Clone)] -pub enum SimCommand { - Step(BigInt), - StepStepSize, + /// Undoes one action. + Undo, + /// Redoes one action. + Redo, + /// Undoes to generation 0. + Reset, + + /// Moves the viewpoint in 2D using relative coordinates. + Move2D(Move2D), + /// Moves the viewpoint in 3D using relative coordinates. + Move3D(Move3D), + /// Zooms in or out by a power of 2. (positive = in, negative = out) + Scale(f64), + /// Zooms in or out by a power of 2, keeping the position at the mouse + /// cursor invariant. + ScaleToCursor(f64), + + /// Snaps the viewpoint to the nearest cell boundary. + SnapPos, + /// Snaps the viewpoint to the nearest power-of-2 scale factor. + SnapScale, + /// Snaps the viewpoint to the nearest power-of-2 scale factor, keeping the + /// position at the mouse cursor invariant. + SnapScaleToCursor, + + /// Moves the viewpoint to the origin. + ResetView, + /// Moves the viewpoint to fit the pattern. + FitView, + /// Moves the viewpoint to cell at the mouse cursor. + FocusCursor, + /// Sets the cell state for drawing. + SetDrawState(u8), + /// Selects the next cell state for drawing. + NextDrawState { wrap: bool }, + /// Selects the previous cell state for drawing. + PrevDrawState { wrap: bool }, + /// Completes a drawing operation. + ConfirmDraw, + /// Cancels a drawing operation. + CancelDraw, + + /// Selects the entire pattern. + SelectAll, + /// Deselects all cells. + Deselect, + /// Copies the selection to the clipboard. + CopySelection(CaFormat), + /// Pastes the clipboard contents as a selection. + PasteSelection, + /// Deletes the selection contents. + DeleteSelection, + /// Drops selected cells onto the pattern or deselects all cells. + CancelSelection, + + /// Advances the simulation by a specific number of generations. + Step(usize), + /// Advances the simulation by the configured step size. + StepStepSize, + /// Starts running the simulation continuously. StartRunning, + /// Stops running the simulation continuously. StopRunning, + /// Toggles running the simulation continuously. ToggleRunning, - + /// Restarts the continuously-running simulation using the newly configured + /// step size value, if it was already running. (hidden) UpdateStepSize, + /// Cancels any pending simulation requests. + CancelSim, - Cancel, + /// Clears the HashLife cache to reduce memory usage. + ClearCache, } -impl_command_from!(Command::Sim(SimCommand)); +impl Cmd { + /// Returns the way to display the mouse cursor when this is the main + /// command for the selected tool. + pub fn mouse_display_mode(&self) -> MouseDisplayMode { + match self { + Self::BeginDrag(cmd) => cmd.mouse_display_mode(), + + _ => MouseDisplayMode::Normal, + } + } + + /// Creates a `CmdMsg` for this command including a mouse cursor position. + pub fn at(self, cursor_pos: FVec2D) -> CmdMsg { + CmdMsg { + command: self, + cursor_pos: Some(cursor_pos), + } + } + /// Creates a `CmdMsg` for this command with no mouse cursor position. This + /// is invalid for some commands, and will log a warning! + pub fn to_msg(self) -> CmdMsg { + match &self { + Cmd::BeginDrag(_) + | Cmd::ContinueDrag + | Cmd::ScaleToCursor(_) + | Cmd::SnapScaleToCursor + | Cmd::FocusCursor => { + error!( + ".to_msg() called on {:?}, which requires cursor position; use .at() instead", + self, + ); + } + _ => (), + } + CmdMsg { + command: self, + cursor_pos: None, + } + } +} + +/// Command issued to a `GridView` that starts a mouse drag. #[derive(Debug, Clone)] -pub enum HistoryCommand { - Undo, - Redo, - UndoTo(BigInt), +pub enum DragCmd { + /// Drag the viewpoint/camera. + View(DragViewCmd), + + /// Draws freeform. + DrawFreeform(DrawMode), + + /// Selects a new rectangle. + SelectNewRect, + /// Resizes the selection to the mouse cursor. + ResizeSelectionToCursor, + /// Resizes the selection in 2D along a cardinal or intercardinal direction. + /// (mouse target) + ResizeSelection2D(Direction), + /// Resizes the selection in 3D along a cardinal direction. (mouse target) + ResizeSelection3D(Face), + /// Moves the selection. (mouse target) + MoveSelection(Option), + /// Moves the selected cells. (mouse target) + MoveSelectedCells(Option), + /// Moves a copy of the selected cells. (mouse target) + CopySelectedCells(Option), } -impl From for Command { - fn from(c: HistoryCommand) -> Self { - Self::History(c) +impl From for Cmd { + fn from(cmd: DragCmd) -> Self { + Cmd::BeginDrag(cmd) } } +impl DragCmd { + /// Returns the way to display the mouse cursor when this is the main + /// command for the selected tool. + pub fn mouse_display_mode(&self) -> MouseDisplayMode { + match self { + Self::View(cmd) => cmd.mouse_display_mode(), -#[derive(Debug, Clone)] -pub enum ViewCommand { - Drag(ViewDragCommand, FVec2D), - - GoTo2D { - x: Option, - y: Option, - relative: bool, - scaled: bool, - }, - GoTo3D { - x: Option, - y: Option, - z: Option, - yaw: Option>, - pitch: Option>, - relative: bool, - scaled: bool, - }, - GoToScale(Scale), - - Scale { - /// Base-2 logarithm of the relative scale factor. - log2_factor: f64, - /// Optional invariant position. - invariant_pos: Option, - }, - - /// Snap viewpoint to the nearest integer cell position. - SnapPos, - /// Snap viewpoint to the nearest power-of-2 scale factor. - SnapScale { - /// Optional invariant position. - invariant_pos: Option, - }, + Self::DrawFreeform(mode) => MouseDisplayMode::Draw(*mode), - FitView, + Self::SelectNewRect => MouseDisplayMode::Select, + Self::ResizeSelectionToCursor => MouseDisplayMode::ResizeSelectionToCursor, + Self::ResizeSelection2D(direction) => MouseDisplayMode::ResizeSelectionEdge(*direction), + Self::ResizeSelection3D(face) => MouseDisplayMode::ResizeSelectionFace(*face), + Self::MoveSelection(_) | Self::MoveSelectedCells(_) | Self::CopySelectedCells(_) => { + MouseDisplayMode::Move + } + } + } + /// Returns `true` if this drag command always waits for the cursor to move + /// a certain threshold distance from the initial click before acting. + /// Returns `false` if the drag command only waits for that treshold when + /// another action is bound on click. + pub fn always_uses_movement_threshold(&self) -> bool { + match self { + Self::View(cmd) => cmd.always_uses_movement_threshold(), + + Self::DrawFreeform(_) => false, + + Self::SelectNewRect => true, + Self::ResizeSelectionToCursor => false, + Self::ResizeSelection2D(_) => true, + Self::ResizeSelection3D(_) => true, + Self::MoveSelection(_) => true, + Self::MoveSelectedCells(_) => true, + Self::CopySelectedCells(_) => true, + } + } + + pub fn is_view_cmd(&self) -> bool { + match self { + DragCmd::View(_) => true, + + DragCmd::DrawFreeform(_) => false, + + DragCmd::SelectNewRect + | DragCmd::ResizeSelectionToCursor + | DragCmd::ResizeSelection2D(_) + | DragCmd::ResizeSelection3D(_) + | DragCmd::MoveSelection(_) + | DragCmd::MoveSelectedCells(_) + | DragCmd::CopySelectedCells(_) => false, + } + } + pub fn is_draw_cmd(&self) -> bool { + match self { + DragCmd::View(_) => false, + + DragCmd::DrawFreeform(_) => true, + + DragCmd::SelectNewRect + | DragCmd::ResizeSelectionToCursor + | DragCmd::ResizeSelection2D(_) + | DragCmd::ResizeSelection3D(_) + | DragCmd::MoveSelection(_) + | DragCmd::MoveSelectedCells(_) + | DragCmd::CopySelectedCells(_) => false, + } + } + pub fn is_select_cmd(&self) -> bool { + match self { + DragCmd::View(_) => false, + + DragCmd::DrawFreeform(_) => false, + + DragCmd::SelectNewRect + | DragCmd::ResizeSelectionToCursor + | DragCmd::ResizeSelection2D(_) + | DragCmd::ResizeSelection3D(_) + | DragCmd::MoveSelection(_) + | DragCmd::MoveSelectedCells(_) + | DragCmd::CopySelectedCells(_) => true, + } + } } -impl_command_from!(Command::View(ViewCommand)); #[derive(Debug, Copy, Clone)] -pub enum ViewDragCommand { - /// Rotates the 3D view around the viewpoint pivot. - Orbit, +pub enum DragViewCmd { + /// Rotates the 3D camera around the viewpoint pivot. + Orbit3D, /// Pans in the plane of the camera. Pan, - /// Pans in the nearest axis-aligned plane. - PanAligned, - /// Pans in the nearest axis-aligned plane parallel to the Y axis. - PanAlignedVertical, /// Pans in the XZ plane. - PanHorizontal, - /// Adjusts scale. + PanHorizontal3D, + /// Zooms in or out. Scale, } +impl From for DragCmd { + fn from(cmd: DragViewCmd) -> Self { + DragCmd::View(cmd) + } +} +impl From for Cmd { + fn from(cmd: DragViewCmd) -> Self { + Cmd::BeginDrag(DragCmd::View(cmd)) + } +} +impl DragViewCmd { + /// Returns the way to display the mouse cursor when this is the main + /// command for the selected tool. + pub fn mouse_display_mode(&self) -> MouseDisplayMode { + match self { + Self::Orbit3D => MouseDisplayMode::Orbit, + Self::Pan | Self::PanHorizontal3D => MouseDisplayMode::Pan, + Self::Scale => MouseDisplayMode::Scale, + } + } + pub fn always_uses_movement_threshold(&self) -> bool { + false + } +} -#[derive(Debug, Clone)] -pub enum DrawCommand { - SetState(u8), - Drag(DrawDragCommand, FVec2D), - - Confirm, - Cancel, +/// 2D movement relative to the camera. +#[derive(Debug, Default, Copy, Clone)] +pub struct Move2D { + pub dx: f64, + pub dy: f64, +} +impl Move2D { + pub fn right(n: f64) -> Self { + Self { dx: n, dy: 0.0 } + } + pub fn left(n: f64) -> Self { + Self { dx: -n, dy: 0.0 } + } + pub fn up(n: f64) -> Self { + Self { dx: 0.0, dy: n } + } + pub fn down(n: f64) -> Self { + Self { dx: 0.0, dy: -n } + } +} +impl From for Cmd { + fn from(move2d: Move2D) -> Self { + Cmd::Move2D(move2d) + } } -impl_command_from!(Command::Draw(DrawCommand)); +/// 3D movement relative to the camera. #[derive(Debug, Copy, Clone)] -pub struct DrawDragCommand { - pub mode: DrawMode, - pub shape: DrawShape, +pub struct Move3D { + pub dx: f64, + pub dy: f64, + pub dz: f64, + pub dyaw: Deg, + pub dpitch: Deg, +} +impl Default for Move3D { + fn default() -> Self { + Self { + dx: 0.0, + dy: 0.0, + dz: 0.0, + dyaw: Deg(0.0), + dpitch: Deg(0.0), + } + } +} +impl From for Cmd { + fn from(move3d: Move3D) -> Self { + Cmd::Move3D(move3d) + } +} +impl Move3D { + pub fn east(n: f64) -> Self { + Self { + dx: n, + ..Default::default() + } + } + pub fn west(n: f64) -> Self { + Self { + dx: -n, + ..Default::default() + } + } + pub fn up(n: f64) -> Self { + Self { + dy: n, + ..Default::default() + } + } + pub fn down(n: f64) -> Self { + Self { + dy: -n, + ..Default::default() + } + } + pub fn north(n: f64) -> Self { + Self { + dz: n, + ..Default::default() + } + } + pub fn south(n: f64) -> Self { + Self { + dz: -n, + ..Default::default() + } + } } -#[derive(Debug, Copy, Clone)] +/// How drawing should affect individual cells, depending on their prior state. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum DrawMode { /// Sets the state of a #0 cell to nonzero. Place, @@ -156,36 +421,19 @@ impl DrawMode { DrawMode::Erase => 0_u8, } } -} - -#[derive(Debug, Copy, Clone)] -pub enum DrawShape { - /// Modifies cells in a freeform path. - Freeform, - /// Modifies cells in a straight line. - Line, -} -#[derive(Debug, Clone)] -pub enum SelectCommand { - Drag(SelectDragCommand, FVec2D), - SelectAll, - Deselect, - - Copy(CaFormat), - Paste, - Delete, - - Cancel, -} -impl_command_from!(Command::Select(SelectCommand)); - -#[derive(Debug, Copy, Clone)] -pub enum SelectDragCommand { - NewRect, - Resize { axes: AxisSet, plane: Option }, - ResizeToCell, - MoveSelection, - MoveCells, - CopyCells, + pub fn fill_color(self) -> Srgba { + match self { + DrawMode::Place => crate::colors::hover::PLACE_FILL, + DrawMode::Replace => crate::colors::hover::REPLACE_FILL, + DrawMode::Erase => crate::colors::hover::ERASE_FILL, + } + } + pub fn outline_color(self) -> Srgb { + match self { + DrawMode::Place => crate::colors::hover::PLACE_OUTLINE, + DrawMode::Replace => crate::colors::hover::REPLACE_OUTLINE, + DrawMode::Erase => crate::colors::hover::ERASE_OUTLINE, + } + } } diff --git a/ui/src/config/ctrl.rs b/ui/src/config/ctrl.rs index 3f25bb85..7e163bb4 100644 --- a/ui/src/config/ctrl.rs +++ b/ui/src/config/ctrl.rs @@ -6,8 +6,8 @@ pub struct CtrlConfig { pub keybd_scale_speed_3d: f64, pub discrete_scale_speed_2d: f64, pub discrete_scale_speed_3d: f64, - pub smooth_scroll_speed_2d: f64, - pub smooth_scroll_speed_3d: f64, + pub pixels_per_2x_scale_2d: f64, + pub pixels_per_2x_scale_3d: f64, pub mouse_orbit_speed: f64, // TODO: make speed_modifier an attribute of the keybind pub speed_modifier: f64, @@ -34,8 +34,8 @@ impl Default for CtrlConfig { keybd_scale_speed_3d: 2.0, discrete_scale_speed_2d: 1.0, discrete_scale_speed_3d: 0.5, - smooth_scroll_speed_2d: 1.0, - smooth_scroll_speed_3d: 0.5, + pixels_per_2x_scale_2d: 100.0, + pixels_per_2x_scale_3d: 200.0, mouse_orbit_speed: 0.75, speed_modifier: 3.0, @@ -50,7 +50,7 @@ impl Default for CtrlConfig { interpolation: Interpolation::default(), - selection_resize_drag_target_width: 8.0, + selection_resize_drag_target_width: 12.0, } } } diff --git a/ui/src/config/gfx.rs b/ui/src/config/gfx.rs index dcf49787..21aacefa 100644 --- a/ui/src/config/gfx.rs +++ b/ui/src/config/gfx.rs @@ -1,10 +1,16 @@ +use palette::Srgba; + #[derive(Debug)] pub struct GfxConfig { pub dpi: f64, pub fps: f64, pub font_size: f32, + pub msaa: Msaa, + pub octree_perf_view: bool, + + pub cell_colors: [Srgba; 256], } impl Default for GfxConfig { fn default() -> Self { @@ -13,7 +19,11 @@ impl Default for GfxConfig { fps: 60.0, font_size: 16.0, + msaa: Msaa::_8, + octree_perf_view: false, + + cell_colors: crate::default_colors(), } } } @@ -23,3 +33,11 @@ impl GfxConfig { std::time::Duration::from_secs_f64(1.0 / self.fps) } } + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Msaa { + Off = 0, + _2 = 2, + _4 = 4, + _8 = 8, +} diff --git a/ui/src/config/hist.rs b/ui/src/config/hist.rs index 3dfb886d..d9d68e68 100644 --- a/ui/src/config/hist.rs +++ b/ui/src/config/hist.rs @@ -1,3 +1,5 @@ +use crate::commands::DragCmd; + #[derive(Debug)] pub struct HistoryConfig { pub record_move_cells: bool, @@ -10,8 +12,24 @@ impl Default for HistoryConfig { fn default() -> Self { Self { record_move_cells: true, - record_select: false, // still restored when moving + record_select: false, record_view: false, } } } +impl HistoryConfig { + pub fn should_record_history_for_drag_command(&self, command: &DragCmd) -> bool { + match command { + DragCmd::View(_) => false, + + DragCmd::DrawFreeform(_) => true, + + DragCmd::SelectNewRect + | DragCmd::ResizeSelectionToCursor + | DragCmd::ResizeSelection2D(_) + | DragCmd::ResizeSelection3D(_) + | DragCmd::MoveSelection(_) => self.record_select, + DragCmd::MoveSelectedCells(_) | DragCmd::CopySelectedCells(_) => self.record_move_cells, + } + } +} diff --git a/ui/src/config/mouse/mod.rs b/ui/src/config/mouse.rs similarity index 58% rename from ui/src/config/mouse/mod.rs rename to ui/src/config/mouse.rs index 28d299ce..0f794b47 100644 --- a/ui/src/config/mouse/mod.rs +++ b/ui/src/config/mouse.rs @@ -1,20 +1,42 @@ -use glium::glutin::event::{ModifiersState, MouseButton}; +use glium::glutin::event::{ModifiersState, MouseButton as GlutinMouseButton}; +use std::convert::{TryFrom, TryInto}; use std::ops::{Index, IndexMut}; -mod click; -mod drag; +use crate::commands::{Cmd, DragCmd, DragViewCmd, DrawMode}; -pub use click::*; -pub use drag::*; +/// Left, right, or middle mouse button. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum MouseButton { + Left, + Right, + Middle, +} +impl Default for MouseButton { + fn default() -> Self { + Self::Left + } +} +impl TryFrom for MouseButton { + type Error = OtherMouseButton; -use crate::commands::{DrawDragCommand, DrawMode, DrawShape, SelectDragCommand, ViewDragCommand}; + fn try_from(button: GlutinMouseButton) -> Result { + match button { + GlutinMouseButton::Left => Ok(Self::Left), + GlutinMouseButton::Right => Ok(Self::Right), + GlutinMouseButton::Middle => Ok(Self::Middle), + GlutinMouseButton::Other(n) => Err(OtherMouseButton(n)), + } + } +} +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +struct OtherMouseButton(u16); #[derive(Debug)] pub struct MouseConfig { - pub click_bindings_2d: MouseBindings>, - pub click_bindings_3d: MouseBindings>, - pub drag_bindings_2d: MouseBindings>, - pub drag_bindings_3d: MouseBindings>, + pub click_bindings_2d: MouseBindings>, + pub click_bindings_3d: MouseBindings>, + pub drag_bindings_2d: MouseBindings>, + pub drag_bindings_3d: MouseBindings>, pub drag_threshold: f64, } impl Default for MouseConfig { @@ -25,43 +47,29 @@ impl Default for MouseConfig { const NONE: ModifiersState = ModifiersState::empty(); use MouseButton::{Left, Middle, Right}; - use MouseDragBinding::{Draw, Select, View}; - - let freeform_2d: DrawDragBinding = DrawDragCommand { - mode: DrawMode::Replace, - shape: DrawShape::Freeform, - } - .into(); - let line_2d: DrawDragBinding = DrawDragCommand { - mode: DrawMode::Replace, - shape: DrawShape::Line, - } - .into(); Self { click_bindings_2d: vec![].into_iter().collect(), click_bindings_3d: vec![].into_iter().collect(), drag_bindings_2d: vec![ - (NONE, Left, Draw(freeform_2d)), - (SHIFT, Left, Draw(line_2d)), - (CTRL, Left, Select(SelectDragCommand::NewRect.into())), - ( - CTRL | SHIFT, - Left, - Select(SelectDragCommand::ResizeToCell.into()), - ), - (NONE, Right, View(ViewDragCommand::Pan.into())), - (CTRL, Right, View(ViewDragCommand::Scale.into())), - (NONE, Middle, View(ViewDragCommand::Pan.into())), + (NONE, Left, DragCmd::DrawFreeform(DrawMode::Replace)), + (CTRL, Left, DragCmd::SelectNewRect), + (CTRL | SHIFT, Left, DragCmd::ResizeSelectionToCursor), + (NONE, Right, DragViewCmd::Pan.into()), + (CTRL, Right, DragViewCmd::Scale.into()), + (NONE, Middle, DragViewCmd::Pan.into()), ] .into_iter() .collect(), drag_bindings_3d: vec![ - (CTRL, Left, Select(SelectDragCommand::NewRect.into())), - (NONE, Right, View(ViewDragCommand::Orbit.into())), - (CTRL, Right, View(ViewDragCommand::Scale.into())), - (NONE, Middle, View(ViewDragCommand::Pan.into())), - (SHIFT, Middle, View(ViewDragCommand::PanHorizontal.into())), + (NONE, Left, DragCmd::DrawFreeform(DrawMode::Place)), + (SHIFT, Left, DragCmd::DrawFreeform(DrawMode::Erase)), + (CTRL, Left, DragCmd::SelectNewRect), + (CTRL | SHIFT, Left, DragCmd::ResizeSelectionToCursor), + (NONE, Right, DragViewCmd::Orbit3D.into()), + (CTRL, Right, DragViewCmd::Scale.into()), + (NONE, Middle, DragViewCmd::Pan.into()), + (SHIFT, Middle, DragViewCmd::PanHorizontal3D.into()), ] .into_iter() .collect(), @@ -75,8 +83,13 @@ impl MouseConfig { &self, ndim: usize, mods: ModifiersState, - button: MouseButton, - ) -> (&Option, &Option) { + button: GlutinMouseButton, + ) -> (&Option, &Option) { + let button = match button.try_into() { + Ok(b) => b, + Err(_) => return (&None, &None), + }; + match ndim { 2 => ( &self.click_bindings_2d[(mods, button)], @@ -119,7 +132,6 @@ impl Index<(ModifiersState, MouseButton)> for MouseBindings { MouseButton::Left => &self[mods].0, MouseButton::Right => &self[mods].1, MouseButton::Middle => &self[mods].2, - _ => panic!("Cannot assign binding to non-standard mouse button"), } } } @@ -129,7 +141,6 @@ impl IndexMut<(ModifiersState, MouseButton)> for MouseBindings { MouseButton::Left => &mut self[mods].0, MouseButton::Right => &mut self[mods].1, MouseButton::Middle => &mut self[mods].2, - _ => panic!("Cannot assign binding to non-standard mouse button"), } } } diff --git a/ui/src/config/mouse/click.rs b/ui/src/config/mouse/click.rs deleted file mode 100644 index febd8806..00000000 --- a/ui/src/config/mouse/click.rs +++ /dev/null @@ -1,9 +0,0 @@ -use crate::mouse::MouseDisplay; - -#[derive(Debug, Clone)] -pub enum MouseClickBinding {} -impl MouseClickBinding { - pub fn display(&self) -> MouseDisplay { - match self.clone() {} - } -} diff --git a/ui/src/config/mouse/drag.rs b/ui/src/config/mouse/drag.rs deleted file mode 100644 index dd9a8375..00000000 --- a/ui/src/config/mouse/drag.rs +++ /dev/null @@ -1,81 +0,0 @@ -use ndcell_core::ndvec::FVec2D; - -use crate::commands::*; -use crate::mouse::MouseDisplay; - -#[derive(Debug, Copy, Clone)] -pub enum MouseDragBinding { - Draw(DrawDragBinding), - Select(SelectDragBinding), - View(ViewDragBinding), -} -impl MouseDragBinding { - pub fn display(&self) -> MouseDisplay { - match self { - Self::Draw(_) => MouseDisplay::Draw, - Self::Select(s) => { - use SelectDragCommand::*; - match s.0 { - NewRect => MouseDisplay::Select, - Resize { .. } => MouseDisplay::Normal, - ResizeToCell => MouseDisplay::ResizeSelectionAbsolute, - MoveSelection | MoveCells | CopyCells => MouseDisplay::Move, - } - } - Self::View(v) => { - use ViewDragCommand::*; - match v.0 { - Orbit => MouseDisplay::Normal, // TODO: better mouse icon - Pan | PanAligned | PanAlignedVertical | PanHorizontal => MouseDisplay::Pan, - Scale => MouseDisplay::ResizeNS, // TODO: better mouse icon - } - } - } - } - pub fn to_command(self, cursor_pos: FVec2D) -> Command { - match self { - Self::Draw(b) => b.to_command(cursor_pos), - Self::Select(b) => b.to_command(cursor_pos), - Self::View(b) => b.to_command(cursor_pos), - } - } -} - -#[derive(Debug, Copy, Clone)] -pub struct DrawDragBinding(DrawDragCommand); -impl DrawDragBinding { - pub fn to_command(self, cursor_pos: FVec2D) -> Command { - DrawCommand::Drag(self.0, cursor_pos).into() - } -} -impl From for DrawDragBinding { - fn from(c: DrawDragCommand) -> Self { - Self(c) - } -} - -#[derive(Debug, Copy, Clone)] -pub struct SelectDragBinding(pub SelectDragCommand); -impl SelectDragBinding { - pub fn to_command(self, cursor_pos: FVec2D) -> Command { - SelectCommand::Drag(self.0, cursor_pos).into() - } -} -impl From for SelectDragBinding { - fn from(c: SelectDragCommand) -> Self { - Self(c) - } -} - -#[derive(Debug, Copy, Clone)] -pub struct ViewDragBinding(pub ViewDragCommand); -impl ViewDragBinding { - pub fn to_command(self, cursor_pos: FVec2D) -> Command { - ViewCommand::Drag(self.0, cursor_pos).into() - } -} -impl From for ViewDragBinding { - fn from(c: ViewDragCommand) -> Self { - Self(c) - } -} diff --git a/ui/src/direction.rs b/ui/src/direction.rs new file mode 100644 index 00000000..e4aad2e0 --- /dev/null +++ b/ui/src/direction.rs @@ -0,0 +1,68 @@ +use ndcell_core::prelude::*; + +/// All cardinal and ordical directions. +pub const DIRECTIONS: [Direction; 8] = [ + Direction::N, + Direction::NE, + Direction::E, + Direction::SE, + Direction::S, + Direction::SW, + Direction::W, + Direction::NW, +]; + +/// 2D cardinal or ordinal direction. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum Direction { + /// North. + N, + /// Northeast. + NE, + /// East. + E, + /// Southeast. + SE, + /// South. + S, + /// Southwest. + SW, + /// West. + W, + /// Northwest. + NW, +} +impl Direction { + /// Returns the X component of the vector in this direction (0, -1, or +1). + pub fn x(self) -> isize { + match self { + Direction::N | Direction::S => 0, + Direction::NE | Direction::E | Direction::SE => 1, + Direction::SW | Direction::W | Direction::NW => -1, + } + } + /// Returns the Y component of the vector in this direction (0, -1, or +1). + pub fn y(self) -> isize { + match self { + Direction::E | Direction::W => 0, + Direction::NW | Direction::N | Direction::NE => 1, + Direction::SE | Direction::S | Direction::SW => -1, + } + } + /// Returns the vector in this direction where each component is either 0, + /// -1, or +1. + pub fn vector(self) -> IVec2D { + NdVec([self.x(), self.y()]) + } + /// Returns the set of nonzero axes for the vector in this direction. + pub fn axes(self) -> AxisSet { + let mut ret = AxisSet::empty(); + if self.x() != 0 { + ret.add(Axis::X); + } + if self.y() != 0 { + ret.add(Axis::Y); + } + ret + } +} diff --git a/ui/src/ext.rs b/ui/src/ext.rs index 4f9daedb..53f8c302 100644 --- a/ui/src/ext.rs +++ b/ui/src/ext.rs @@ -71,3 +71,52 @@ impl IVecConvertExt for IVec3D { [self[X] as i32, self[Y] as i32, self[Z] as i32] } } + +pub trait SliceGroupByExt<'a, T> { + fn group_by K>(&'a self, key: F) -> SliceGroupIter<'a, T, F>; +} +impl<'a, T> SliceGroupByExt<'a, T> for [T] { + fn group_by K>(&'a self, key: F) -> SliceGroupIter<'a, T, F> { + let remaining = self; + SliceGroupIter { remaining, key } + } +} +pub struct SliceGroupIter<'a, T, F> { + remaining: &'a [T], + key: F, +} +impl<'a, T, K: PartialEq, F: FnMut(&'a T) -> K> Iterator for SliceGroupIter<'a, T, F> { + type Item = (K, &'a [T]); + + fn next(&mut self) -> Option { + if self.remaining.is_empty() { + return None; + } + let current_key = (self.key)(&self.remaining[0]); + let i = self + .remaining + .iter() + .take_while(|t| (self.key)(t) == current_key) + .count(); + let (current_slice, remaining) = self.remaining.split_at(i); + self.remaining = remaining; + Some((current_key, current_slice)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_slice_group_by() { + let xs = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; + let mut groups = xs.group_by(|x| x / 5); + assert_eq!(groups.next(), Some((0, &xs[0..5]))); + assert_eq!(groups.next(), Some((1, &xs[5..10]))); + assert_eq!(groups.next(), Some((2, &xs[10..15]))); + assert_eq!(groups.next(), Some((3, &xs[15..]))); + assert_eq!(groups.next(), None); + assert_eq!(groups.next(), None); + } +} diff --git a/ui/src/face.rs b/ui/src/face.rs new file mode 100644 index 00000000..00bd5b21 --- /dev/null +++ b/ui/src/face.rs @@ -0,0 +1,176 @@ +use std::fmt; + +use ndcell_core::prelude::*; +use Axis::{X, Y, Z}; + +use crate::Plane; + +pub const FACES: [Face; 6] = [ + Face::PosX, + Face::PosY, + Face::PosZ, + Face::NegX, + Face::NegY, + Face::NegZ, +]; + +/// Axis-aligned 3D cube face. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Face { + PosX, + PosY, + PosZ, + NegX, + NegY, + NegZ, +} +impl fmt::Display for Face { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use Face::*; + match self { + PosX => write!(f, "+X"), + PosY => write!(f, "+Y"), + PosZ => write!(f, "+Z"), + NegX => write!(f, "-X"), + NegY => write!(f, "-Y"), + NegZ => write!(f, "-Z"), + } + } +} +impl Face { + /// Returns a face with a positive normal along an axis. + pub fn positive(axis: Axis) -> Self { + use Face::*; + match axis { + X => PosX, + Y => PosY, + Z => PosZ, + _ => panic!("Invalid 3D axis: {:?}", axis), + } + } + /// Returns a face with a negative normal along an axis. + pub fn negative(axis: Axis) -> Self { + use Face::*; + match axis { + X => NegX, + Y => NegY, + Z => NegZ, + _ => panic!("Invalid 3D axis: {:?}", axis), + } + } + /// Returns the opposite face. + pub fn opposite(self) -> Self { + use Face::*; + match self { + PosX => NegX, + PosY => NegY, + PosZ => NegZ, + NegX => PosX, + NegY => PosY, + NegZ => PosZ, + } + } + + /// Returns the sign of the nonzero component of the normal vector. + pub fn sign(self) -> Sign { + use Face::*; + match self { + PosX | PosY | PosZ => Sign::Plus, + NegX | NegY | NegZ => Sign::Minus, + } + } + /// Returns the normal axis. + pub fn normal_axis(self) -> Axis { + use Face::*; + match self { + PosX | NegX => X, + PosY | NegY => Y, + PosZ | NegZ => Z, + } + } + /// Returns the two perpendicular axes, in order. + pub fn plane_axes(self) -> [Axis; 2] { + use Face::*; + match self { + PosX => [Y, Z], + PosY => [Z, X], + PosZ => [X, Y], + NegX => [Z, Y], + NegY => [X, Z], + NegZ => [Y, X], + } + } + + /// Returns the normal vector, which has a single nonzero component that is + /// either +1 or -1. + pub fn normal(self) -> [i8; 3] { + use Face::*; + match self { + PosX => [1, 0, 0], + PosY => [0, 1, 0], + PosZ => [0, 0, 1], + NegX => [-1, 0, 0], + NegY => [0, -1, 0], + NegZ => [0, 0, -1], + } + } + /// Returns the normal vector, which has a single nonzero component that is + /// either +1 or -1. + pub fn normal_ivec(self) -> IVec3D { + let [x, y, z] = self.normal(); + NdVec([x as isize, y as isize, z as isize]) + } + /// Returns the normal vector, which has a single nonzero component that is + /// either +1 or -1. + pub fn normal_fvec(self) -> FVec3D { + let [x, y, z] = self.normal(); + NdVec([r64(x as f64), r64(y as f64), r64(z as f64)]) + } + /// Returns the normal vector, which has a single nonzero component that is + /// either +1 or -1. + pub fn normal_bigvec(self) -> BigVec3D { + let [x, y, z] = self.normal(); + NdVec([x.into(), y.into(), z.into()]) + } + + /// Returns a cuboid flattened to this face of itself. + pub fn of(self, cuboid: FRect3D) -> FRect3D { + let mut min = cuboid.min(); + let mut max = cuboid.max(); + let axis = self.normal_axis(); + match self.sign() { + Sign::Minus => max[axis] = min[axis], + Sign::NoSign => unreachable!(), + Sign::Plus => min[axis] = max[axis], + } + FRect3D::span(min, max) + } + + /// Returns the corners of this face of the cuboid. + pub fn corners_of(self, cuboid: FRect3D) -> [FVec3D; 4] { + let rect = self.of(cuboid); + let min = rect.min(); + let max = rect.max(); + let [ax1, ax2] = self.plane_axes(); + let mut ret = [min; 4]; + ret[1][ax1] = max[ax1]; + ret[2][ax2] = max[ax2]; + ret[3][ax1] = max[ax1]; + ret[3][ax2] = max[ax2]; + ret + } + + /// Returns the plane of this face of the cuboid. + pub fn plane_of(self, cuboid: &FixedRect3D) -> Plane { + let axis = self.normal_axis(); + let coordinate = match self { + Face::PosX => cuboid.max()[X].clone(), + Face::PosY => cuboid.max()[Y].clone(), + Face::PosZ => cuboid.max()[Z].clone(), + Face::NegX => cuboid.min()[X].clone(), + Face::NegY => cuboid.min()[Y].clone(), + Face::NegZ => cuboid.min()[Z].clone(), + }; + Plane { axis, coordinate } + } +} diff --git a/ui/src/math/bresenham.rs b/ui/src/gridview/algorithms/bresenham.rs similarity index 94% rename from ui/src/math/bresenham.rs rename to ui/src/gridview/algorithms/bresenham.rs index 2cc59a27..f5bc6d89 100644 --- a/ui/src/math/bresenham.rs +++ b/ui/src/gridview/algorithms/bresenham.rs @@ -101,15 +101,15 @@ mod tests { /// point implementation. #[test] fn test_bresenham( - x0 in -100..=100_isize, - y0 in -100..=100_isize, - z0 in -100..=100_isize, x1 in -100..=100_isize, y1 in -100..=100_isize, z1 in -100..=100_isize, + x2 in -100..=100_isize, + y2 in -100..=100_isize, + z2 in -100..=100_isize, ) { - let start: IVec3D = NdVec([x0, y0, z0]); - let end: IVec3D = NdVec([x1, y1, z1]); + let start: IVec3D = NdVec([x1, y1, z1]); + let end: IVec3D = NdVec([x2, y2, z2]); test_bresenham_single_line(start, end); } } diff --git a/ui/src/math/mod.rs b/ui/src/gridview/algorithms/mod.rs similarity index 100% rename from ui/src/math/mod.rs rename to ui/src/gridview/algorithms/mod.rs diff --git a/ui/src/math/raycast.rs b/ui/src/gridview/algorithms/raycast.rs similarity index 51% rename from ui/src/math/raycast.rs rename to ui/src/gridview/algorithms/raycast.rs index 9153bc44..cd981085 100644 --- a/ui/src/math/raycast.rs +++ b/ui/src/gridview/algorithms/raycast.rs @@ -3,9 +3,13 @@ //! //! This is also implemented in GLSL in the octree fragment shader. +// TODO: rewrite using cgmath so that division by zero is safe + use ndcell_core::prelude::*; -#[derive(Debug, Copy, Clone)] +use crate::Face; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct Hit { pub start: FVec3D, pub delta: FVec3D, @@ -15,14 +19,20 @@ pub struct Hit { pub pos_int: IVec3D, pub pos_float: FVec3D, - - pub face_axis: Axis, - pub face_sign: Sign, + pub face: Face, +} +impl Hit { + pub fn add_base_pos(mut self, base_pos: IVec3D) -> Self { + self.start += base_pos.to_fvec(); + self.pos_int += base_pos; + self.pos_float += base_pos.to_fvec(); + self + } } -/// Computes a 3D octree raycast. `start` and `delta` are both in units of -/// `min_layer` nodes, and `start` is relative to the lower corner of `node`. -pub fn octree_raycast( +/// Computes a 3D raycast into an octree. `start` and `delta` are both in units +/// of `min_layer` nodes, and `start` is relative to the lower corner of `node`. +pub fn intersect_octree( mut start: FVec3D, mut delta: FVec3D, min_layer: Layer, @@ -30,6 +40,9 @@ pub fn octree_raycast( ) -> Option { let node_len = r64((node.layer() - min_layer).big_len().to_f64().unwrap()); + let original_start = start; + let original_delta = delta; + // Check each component of the delta vector to see if it's negative. If it // is, then mirror the ray along that axis so that the delta vector is // positive and also mirror the quadtree along that axis using @@ -42,6 +55,8 @@ pub fn octree_raycast( delta[ax] = -delta[ax]; } } + // Please don't crash on division by zero! + ensure_nonzero_delta(&mut delta); // At what `t` does the ray enter the root node (considering only one axis // at a time)? @@ -60,7 +75,33 @@ pub fn octree_raycast( return None; } else { // Otherwise, the ray does intersect with the root node. - return raycast_node_child(start, delta, t0, t1, min_layer, node, invert_mask); + raycast_node_child(start, delta, t0, t1, min_layer, node, invert_mask).map(|(t0, t1)| { + let tm = (t0 + t1) / 2.0; + + let max_t0: R64 = *t0.max_component(); + let min_t1: R64 = *t1.min_component(); + + let pos_int = (original_start + original_delta * tm).floor().to_ivec(); + let pos_float = original_start + original_delta * max_t0; + let entry_axis = entry_axis(t0); + let face = if invert_mask & entry_axis.bit() == 0 { + Face::negative(entry_axis) // ray is positive; hit negative face of cell + } else { + Face::positive(entry_axis) // ray is negative; hit positive face of cell + }; + + Hit { + start, + delta, + + t0: max_t0, + t1: min_t1, + + pos_int, + pos_float, + face, + } + }) } } @@ -72,7 +113,7 @@ fn raycast_node_child( min_layer: Layer, node: NodeRef<'_, Dim3D>, invert_mask: usize, -) -> Option { +) -> Option<(FVec3D, FVec3D)> { if *t1.min_component() < r64(0.0) { // This node is completely behind the ray; skip it. return None; @@ -92,28 +133,7 @@ fn raycast_node_child( if node.layer() <= min_layer { // This is a nonzero leaf node, so return a hit. - let pos_int = (start + delta * tm).floor().to_ivec(); - let pos_float = start + delta * t0; - let face_axis = entry_axis(t0); - let face_sign = if invert_mask & face_axis.bit() == 0 { - Sign::Minus // ray is positive; hit negative face of cell - } else { - Sign::Plus // ray is negative; hit positive face of cell - }; - - return Some(Hit { - start, - delta, - - t0: *t0.max_component(), - t1: *t1.min_component(), - - pos_int, - pos_float, - - face_axis, - face_sign, - }); + return Some((t0, t1)); } let children = node.subdivide().unwrap(); @@ -155,6 +175,95 @@ fn raycast_node_child( } } +/// Computes the intersection of a 3D ray and a plane. +pub fn intersect_plane( + start: FVec3D, + delta: FVec3D, + perpendicular_axis: Axis, + perpendicular_coordinate: R64, +) -> Option { + if delta[perpendicular_axis].is_zero() { + return None; // The delta vector is parallel to the plane. + } + + let t = (perpendicular_coordinate - start[perpendicular_axis]) / delta[perpendicular_axis]; + if t <= 0.0 { + return None; // The plane is behind the vector. + } + + let face = if delta[perpendicular_axis] > 0.0 { + Face::negative(perpendicular_axis) + } else { + Face::positive(perpendicular_axis) + }; + let pos_float = start + delta * t; + let pos_int = (pos_float - face.normal_fvec() / 2.0).floor().to_ivec(); + + Some(Hit { + start, + delta, + + t0: t, + t1: t, + + pos_int, + pos_float, + face, + }) +} + +/// Computes the intersection of a 3D ray and a cuboid. +pub fn intersect_cuboid(start: FVec3D, mut delta: FVec3D, cuboid: FRect3D) -> Option { + let mut entry_corner = cuboid.min(); + let mut exit_corner = cuboid.max(); + + for &ax in Dim3D::axes() { + if delta[ax].is_negative() { + std::mem::swap(&mut entry_corner[ax], &mut exit_corner[ax]); + } + } + // Please don't crash on division by zero! + ensure_nonzero_delta(&mut delta); + + // At what `t` does the ray enter the cuboid (considering only one axis at a + // time)? + let t0 = (entry_corner - start) / delta; + // At what `t` does the ray exit the cuboid (considering only one axis at + // a time)? + let t1 = (exit_corner - start) / delta; + + // At what `t` has the ray entered the cuboid along all axes? + let max_t0: R64 = *t0.max_component(); + // At what `t` has the ray entered the cuboid along all axes? + let min_t1: R64 = *t1.min_component(); + + // If we enter AFTER we exit, or exit at a negative `t` ... + if max_t0 >= min_t1 || r64(0.0) > min_t1 { + // ... then the ray does not intersect with the cuboid. + return None; + } else { + // Otherwise, the ray does intersect with the cuboid. + let pos_int = (start + delta * (max_t0 + 0.5)).floor().to_ivec(); + let pos_float = start + delta * max_t0; + let entry_axis = entry_axis(t0); + let face = if delta[entry_axis].is_positive() { + Face::negative(entry_axis) // ray is positive; hit negative face of cell + } else { + Face::positive(entry_axis) // ray is negative; hit positive face of cell + }; + + Some(Hit { + start, + delta, + t0: max_t0, + t1: min_t1, + pos_int, + pos_float, + face, + }) + } +} + // Given the parameters `t0` at which the ray enters a node along each axis and // `tm` at which the ray crosses the middle of a node along each axis, returns // the child index of the first child of that node intersected by the ray. @@ -180,3 +289,14 @@ fn entry_axis(t0: FVec3D) -> Axis { fn exit_axis(t1: FVec3D) -> Axis { t1.min_axis() } + +/// Ensure that each component of the delta vector is nonzero by adjusting them +/// slightly if necessary. +fn ensure_nonzero_delta(delta: &mut FVec3D) { + const EPSILON: f64 = std::f32::EPSILON as f64; + for &ax in Dim3D::axes() { + if delta[ax].abs() < EPSILON { + delta[ax] = r64(EPSILON); + } + } +} diff --git a/ui/src/gridview/drag.rs b/ui/src/gridview/drag.rs new file mode 100644 index 00000000..35e8fe1d --- /dev/null +++ b/ui/src/gridview/drag.rs @@ -0,0 +1,109 @@ +use anyhow::{anyhow, Result}; + +use ndcell_core::prelude::*; + +use super::generic::{GenericGridView, GridViewDimension}; +use super::screenpos::{OperationPos, OperationPosTrait, ScreenPosTrait}; +use crate::commands::DragCmd; +use crate::mouse::MouseDisplayMode; +use crate::CONFIG; + +/// Closure for mouse drag events, created when the user starts dragging and +/// called for each cursor movement until released. Returns whether to continue +/// or cancel the drag. +/// +/// It takes as input the `Drag` struct, the current state (which is mutated), +/// and the latest mouse cursor position. If any initial state or mouse cursor +/// history is relevant, the closure must maintain this information. +pub type DragUpdateFn> = + Box, &mut T, &::ScreenPos) -> Result>; + +pub type DragCancelFn = Box; + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum DragOutcome { + Continue, + Cancel, +} + +pub struct Drag { + pub command: DragCmd, + pub(super) initial_screen_pos: D::ScreenPos, + pub waiting_for_drag_threshold: bool, + + pub(super) update_fn: Option>, + pub(super) cancel_fn: Option>>, + + pub(super) ndtree_base_pos: BigVec, +} +impl Drag { + /// Returns the way to display the mouse cursor during the drag. + pub fn mouse_display_mode(&self) -> MouseDisplayMode { + self.command.mouse_display_mode() + } + + /// Returns the cell initially clicked on. + pub fn initial_pos(&self) -> Option> { + self.initial_screen_pos + .op_pos_for_drag_command(&self.command) + } + /// Returns the cell dragged over at `new_screen_pos`. + pub fn new_pos(&self, new_screen_pos: &D::ScreenPos) -> Option> { + new_screen_pos.op_pos_for_continue_drag_command(&self.command, &self.initial_screen_pos) + } + + /// Returns the rectangle of the render cell initially clicked on. + pub fn initial_render_cell_rect(&self) -> Option> { + self.initial_pos().map(|op_pos| { + self.initial_screen_pos.layer().round_rect_with_base_pos( + BigRect::single_cell(op_pos.cell().clone()), + &self.ndtree_base_pos, + ) + }) + } + /// Returns the rectangle of the render cell dragged over at + /// `new_screen_pos`. + pub fn new_render_cell_rect(&self, new_screen_pos: &D::ScreenPos) -> Option> { + self.new_pos(new_screen_pos).map(|op_pos| { + self.initial_screen_pos.layer().round_rect_with_base_pos( + BigRect::single_cell(op_pos.cell().clone()), + &self.ndtree_base_pos, + ) + }) + } + + /// Returns cell highlight data for the cell dragged over at + /// `new_screen_pos`. + pub fn cell_highlight(&self, new_screen_pos: &D::ScreenPos) -> Option> { + new_screen_pos.op_pos_for_continue_drag_command(&self.command, &self.initial_screen_pos) + } + + /// Updates a gridview for a continuation of the drag. + pub fn update( + &mut self, + gridview: &mut GenericGridView, + new_screen_pos: &D::ScreenPos, + ) -> Result { + let config = CONFIG.lock(); + if self.waiting_for_drag_threshold { + if *(new_screen_pos.pixel() - self.initial_screen_pos.pixel()) + .abs() + .max_component() + < config.mouse.drag_threshold + { + return Ok(DragOutcome::Continue); + } else { + self.waiting_for_drag_threshold = false; + } + } + drop(config); // `CONFIG` might be used by `update_fn` + + let mut update_fn = self + .update_fn + .take() + .ok_or(anyhow!("Drag update function mysteriously vanished"))?; + let result = update_fn(self, gridview, new_screen_pos); + self.update_fn = Some(update_fn); + result + } +} diff --git a/ui/src/gridview/generic.rs b/ui/src/gridview/generic.rs index ec43c36a..863e38de 100644 --- a/ui/src/gridview/generic.rs +++ b/ui/src/gridview/generic.rs @@ -1,6 +1,6 @@ -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, bail, ensure, Context, Result}; use glium::Surface; -use log::{debug, trace, warn}; +use log::{debug, error, info, trace, warn}; use parking_lot::Mutex; use std::collections::VecDeque; use std::convert::TryInto; @@ -10,14 +10,18 @@ use std::time::{Duration, Instant}; use ndcell_core::prelude::*; +use super::algorithms::bresenham; +use super::drag::{Drag, DragCancelFn, DragOutcome, DragUpdateFn}; use super::history::{History, HistoryBase, HistoryManager}; use super::render::{RenderParams, RenderResult}; +use super::screenpos::{OperationPos, OperationPosTrait, ScreenPosTrait}; use super::selection::Selection; -use super::viewpoint::{Interpolate, Interpolator, Viewpoint}; +use super::viewpoint::{Interpolator, Viewpoint}; use super::worker::*; -use super::{DragHandler, DragOutcome, DragType, WorkType}; +use super::WorkType; use crate::commands::*; -use crate::CONFIG; +use crate::mouse::MouseState; +use crate::{Face, Scale, CONFIG}; /// Number of previous frame times to track. If this is too low, viewpoint /// interpolation may not work. @@ -26,16 +30,29 @@ const MAX_LAST_FRAME_TIMES: usize = 2; /// steps per second. const MAX_LAST_SIM_TIMES: usize = 4; +macro_rules! ignore_command { + ($c:expr) => {{ + warn!("Ignoring {:?} in GridView{}D", $c, D::NDIM); + return; + }}; + ($c:expr, if $cond:expr) => {{ + if $cond { + ignore_command!($c); + } + }}; +} + /// Dimension-generic interactive cellular automaton interface. -pub struct GenericGridView { - pub automaton: NdAutomaton, - pub selection: Option>, - pub viewpoint_interpolator: Interpolator, - history: HistoryManager>, - dimensionality: G, +pub struct GenericGridView { + pub automaton: NdAutomaton, + pub selection: Option>, + pub viewpoint_interpolator: Interpolator, + history: HistoryManager>, + /// Dimension-specific data. + pub(super) dim_data: D::Data, /// Queue of pending commands to be executed on the next frame. - command_queue: Mutex>, + command_queue: Mutex>, /// Thread to offload long-running computations onto. worker_thread: WorkerThread, /// What kind of work the worker thread is doing right now. @@ -44,10 +61,8 @@ pub struct GenericGridView { /// Communication channel with the garbage collection thread. gc_channel: Option>, - /// Mouse drag handler. - drag_handler: Option>, - /// Type of mouse drag being handled. - drag_type: Option, + /// Mouse drag in progress. + drag: Option>, /// Time that the last several frames completed. last_frame_times: VecDeque, @@ -59,22 +74,22 @@ pub struct GenericGridView { /// Selected cell state. pub selected_cell_state: u8, } -impl From> for GenericGridView { - fn from(automaton: NdAutomaton) -> Self { +impl From> for GenericGridView { + fn from(automaton: NdAutomaton) -> Self { Self { automaton, ..Default::default() } } } -impl Default for GenericGridView { +impl Default for GenericGridView { fn default() -> Self { Self { automaton: Default::default(), selection: Default::default(), viewpoint_interpolator: Default::default(), history: Default::default(), - dimensionality: Default::default(), + dim_data: Default::default(), command_queue: Default::default(), worker_thread: Default::default(), @@ -82,8 +97,7 @@ impl Default for GenericGridView { gc_channel: Default::default(), - drag_handler: Default::default(), - drag_type: Default::default(), + drag: Default::default(), last_frame_times: Default::default(), last_sim_times: Default::default(), @@ -94,7 +108,7 @@ impl Default for GenericGridView { } } } -impl AsSimulate for GenericGridView { +impl AsSimulate for GenericGridView { fn as_sim(&self) -> &dyn Simulate { &self.automaton } @@ -102,8 +116,8 @@ impl AsSimulate for GenericGridView { &mut self.automaton } } -impl HistoryBase for GenericGridView { - type Entry = HistoryEntry; +impl HistoryBase for GenericGridView { + type Entry = HistoryEntry; fn history_entry(&self) -> Self::Entry { HistoryEntry { @@ -118,9 +132,9 @@ impl HistoryBase for GenericGridView { automaton: std::mem::replace(&mut self.automaton, entry.automaton), selection: Selection::restore_history_entry(&mut self.selection, entry.selection), viewpoint: if CONFIG.lock().hist.record_view { - std::mem::replace(&mut self.viewpoint_interpolator.target, entry.viewpoint) + std::mem::replace(self.target_viewpoint(), entry.viewpoint) } else { - self.viewpoint_interpolator.target.clone() + self.target_viewpoint().clone() }, } } @@ -132,11 +146,11 @@ impl HistoryBase for GenericGridView { &mut self.history } } -impl GenericGridView { +impl GenericGridView { /// Enqueues a command to be executed on the next frame. /// /// This should be preferred to executing commands immediately. - pub fn enqueue(&self, command: impl Into) { + pub fn enqueue(&self, command: impl Into) { self.command_queue.lock().push_back(command.into()); } @@ -162,7 +176,7 @@ impl GenericGridView { self.set_new_values(work_type, new_values?)?; self.work_type = None; } - Err(WorkerIdle) => return Err(anyhow!("Worker is idle but work type is not None")), + Err(WorkerIdle) => bail!("Worker is idle but work type is not None"), } } @@ -174,8 +188,8 @@ impl GenericGridView { // Execute commands. let old_command_queue = std::mem::replace(self.command_queue.get_mut(), VecDeque::new()); - for command in old_command_queue { - self.do_command(command)?; + for cmd_msg in old_command_queue { + self.do_command(cmd_msg)?; } // Trigger breakpoint. @@ -200,146 +214,514 @@ impl GenericGridView { Ok(()) } - /// Executes a `Command`. - pub(super) fn do_command(&mut self, command: impl Into) -> Result<()> { - match command.into() { - Command::Sim(c) => self.do_sim_command(c), - Command::History(c) => self.do_history_command(c), - Command::View(c) => self.do_view_command(c), - Command::Draw(c) => self.do_draw_command(c), - Command::Select(c) => self.do_select_command(c), - Command::GarbageCollect => Ok(self.schedule_gc()), - - Command::ContinueDrag(cursor_pos) => self.continue_drag(cursor_pos), - Command::StopDrag => Ok(self.stop_drag()), - - Command::Cancel => { - if self.reset_worker_thread() { - Ok(()) - } else if self.is_drawing() { - self.do_command(DrawCommand::Cancel) - } else { - self.do_command(SelectCommand::Cancel) - } - } - } - } - /// Executes a `SimCommand`. - fn do_sim_command(&mut self, command: SimCommand) -> Result<()> { + /// Executes a command. + pub(super) fn do_command( + &mut self, + CmdMsg { + command, + cursor_pos, + }: CmdMsg, + ) -> Result<()> { + const MISSING_CURSOR_POS_MSG: &str = "Missing cursor position for command that requires it"; + + let screen_pos = cursor_pos + .clone() + .context(MISSING_CURSOR_POS_MSG) + .map(|p| self.screen_pos(p)); + let cursor_pos = cursor_pos.context(MISSING_CURSOR_POS_MSG); + match command { - SimCommand::Step(step_size) => { - self.try_step(step_size)?; + Cmd::BeginDrag(cmd) => Ok(self.begin_drag(cmd, cursor_pos?)), + Cmd::ContinueDrag => self.continue_drag(cursor_pos?), + Cmd::EndDrag => Ok(self.end_drag()), + Cmd::CancelDrag => Ok(self.cancel_drag()), + + Cmd::Cancel => Ok(self.cancel()), + + Cmd::Undo => { + self.end_drag(); + self.reset_worker_thread(); + self.undo(); + Ok(()) } - SimCommand::StepStepSize => { - self.try_step(CONFIG.lock().sim.step_size.clone())?; + Cmd::Redo => { + self.end_drag(); + self.reset_worker_thread(); + self.redo(); + Ok(()) } + Cmd::Reset => { + self.end_drag(); + self.reset_worker_thread(); + self.reset(); + Ok(()) + } + + Cmd::Move2D(movement) => Ok(self.target_viewpoint().apply_move_2d(movement)), + Cmd::Move3D(movement) => Ok(self.target_viewpoint().apply_move_3d(movement)), + Cmd::Scale(log2_factor) => Ok(self + .viewpoint_interpolator + .target + .scale_by_log2_factor(r64(log2_factor), None)), + Cmd::ScaleToCursor(log2_factor) => Ok(self + .target_viewpoint() + .scale_by_log2_factor(r64(log2_factor), screen_pos?.scale_invariant_pos())), + + Cmd::SnapPos => Ok(self.target_viewpoint().snap_center()), + Cmd::SnapScale => Ok(self.target_viewpoint().snap_scale(None)), + Cmd::SnapScaleToCursor => Ok(self + .target_viewpoint() + .snap_scale(screen_pos?.scale_invariant_pos())), + + Cmd::ResetView => Ok(self.go_to_origin()), + Cmd::FitView => Ok(self.fit_view()), + Cmd::FocusCursor => Ok(D::focus(self, &screen_pos?)), + + Cmd::SetDrawState(state) => Ok(self.selected_cell_state = state), + Cmd::NextDrawState { wrap } => Ok(self.select_next_cell_state(1, wrap)), + Cmd::PrevDrawState { wrap } => Ok(self.select_next_cell_state(-1, wrap)), + Cmd::ConfirmDraw => Ok(self.confirm_draw()), + Cmd::CancelDraw => Ok(self.cancel_draw()), - SimCommand::StartRunning => { - // If this fails (e.g. because the user is drawing), ignore the - // error. - let _ = self.start_running(); + Cmd::SelectAll => Ok(self.select_all()), + Cmd::Deselect => { + self.deselect(); + Ok(()) } - SimCommand::StopRunning => { + Cmd::CopySelection(format) => self.copy_selection(format), + Cmd::PasteSelection => self.paste_selection(), + Cmd::DeleteSelection => Ok(self.delete_selection()), + Cmd::CancelSelection => Ok(self.cancel_selection()), + + // For all the simulation-related commands, ignore failures due to + // existing background tasks, drawing, etc. + Cmd::Step(step_size) => self.step(step_size.into()), + Cmd::StepStepSize => self.step(CONFIG.lock().sim.step_size.clone()), + Cmd::StartRunning => self.start_running().map(|_is_running| ()), + Cmd::StopRunning => { self.stop_running(); + Ok(()) } - SimCommand::ToggleRunning => { - if self.is_running() { - self.stop_running(); - } else { - // If this fails, ignore the error. - let _ = self.start_running(); - } - } + Cmd::ToggleRunning => self.toggle_running().map(|_is_running| ()), + Cmd::UpdateStepSize => self.update_step_size(), + Cmd::CancelSim => Ok(self.cancel_sim()), - SimCommand::UpdateStepSize => { - if self.is_running() { - self.stop_running(); - // This should not fail. - self.start_running()?; - } - } + Cmd::ClearCache => Ok(self.schedule_gc()), + } + } - SimCommand::Cancel => { - if let Some(work_type) = self.work_type { - match work_type { - WorkType::SimStep | WorkType::SimContinuous => { - self.reset_worker_thread(); - } - } - } + /// Executes a `Cancel` command. + fn cancel(&mut self) { + if self.is_dragging() { + self.cancel_drag(); + } else if self.reset_worker_thread() { + // ok + } else if self.is_drawing() { + self.cancel_draw() + } else { + self.cancel_selection() + } + } + /// Executes a `CancelDrag` command. + fn cancel_drag(&mut self) { + if let Some(drag) = self.drag.take() { + if let Some(cancel_fn) = drag.cancel_fn { + cancel_fn(self); } } - Ok(()) } - /// Executes a `HistoryCommand`. - fn do_history_command(&mut self, command: HistoryCommand) -> Result<()> { + /// Executes a `CancelDraw` command. + fn cancel_draw(&mut self) { if self.is_drawing() { - trace!("Ignoring {:?} command while drawing", command); - return Ok(()); + self.cancel_drag(); } - match command { - HistoryCommand::Undo => { - self.reset_worker_thread(); - self.undo(); - } - HistoryCommand::Redo => { - if self.can_redo() { - self.reset_worker_thread(); - self.redo(); - } + } + /// Executes a `CancelSelection` command. + fn cancel_selection(&mut self) { + if let Some(sel) = &self.selection { + if sel.cells.is_some() { + self.drop_selected_cells(); + } else { + self.deselect(); } - // TODO make this JumpTo instead of UndoTo - HistoryCommand::UndoTo(gen) => { + } + } + /// Executes a `CancelSim` command. + fn cancel_sim(&mut self) { + match self.work_type { + Some(WorkType::SimStep) | Some(WorkType::SimContinuous) => { self.reset_worker_thread(); - while self.generation_count() > &gen && self.can_undo() { - self.undo(); + } + None => (), + } + } + + /// Executes a `ConfirmDraw` command. + fn confirm_draw(&mut self) { + if self.is_drawing() { + self.end_drag(); + } + } + + /// Executes a `Reset` command. + fn reset(&mut self) { + // TODO: Instead of looping, track the oldest known state or do binary + // search on history stack. + while self.automaton.generation_count().is_positive() && self.can_undo() { + self.undo(); + } + } + + /// Executes a `GoToOrigin` command. + fn go_to_origin(&mut self) { + self.target_viewpoint().set_center(NdVec::origin()); + } + /// Executes a `FitView` command. + fn fit_view(&mut self) { + if let Some(pattern_bounding_rect) = self.automaton.ndtree.bounding_rect() { + // Set position. + let center = pattern_bounding_rect.center().to_fixedvec(); + self.target_viewpoint().set_center(center); + // Set scale. + let pattern_size = pattern_bounding_rect.size(); + let target_size = self.viewpoint().target_dimensions(); + let scale = Scale::from_fit(pattern_size, target_size).floor(); + self.target_viewpoint().set_scale(scale); + } + } + + /// Selects the next or previous cell state (depending on the sign of + /// `delta`), optionally using wrapping arithmetic. + fn select_next_cell_state(&mut self, delta: isize, wrap: bool) { + let mut new_cell_state = self.selected_cell_state as isize + delta; + let max_state = self.automaton.rule.max_state() as isize; + let state_count = max_state + 1; + if wrap { + new_cell_state = new_cell_state.rem_euclid(state_count); + } else { + new_cell_state = new_cell_state.clamp(0, max_state); + } + self.selected_cell_state = new_cell_state as u8; + assert!(self.selected_cell_state <= max_state as u8); + } + + /// Executes a `CopySelection` command. + fn copy_selection(&mut self, format: CaFormat) -> Result<()> { + if self.selection.is_some() { + let s = self + .export(format) + .context("Error while serializing pattern")?; + crate::clipboard_compat::clipboard_set(s)?; + } + + Ok(()) + } + /// Executes a `PasteSelection` command. + fn paste_selection(&mut self) -> Result<()> { + let old_sel_rect = self.selection_rect(); + + self.record(); + let string_from_clipboard = crate::clipboard_compat::clipboard_get()?; + let result = Selection::from_str(&string_from_clipboard, self.automaton.ndtree.pool()); + match result { + Ok(sel) => { + self.set_selection(sel); + self.ensure_selection_visible(); + + // If selection size is the same, preserve position. + if let Some((old_rect, new_sel)) = old_sel_rect.zip(self.selection.as_mut()) { + if old_rect.size() == new_sel.rect.size() { + *new_sel = new_sel.move_by(old_rect.min() - new_sel.rect.min()); + } } } + Err(errors) => info!("Failed to load pattern: {:?}", errors), } + Ok(()) } - /// Executes a `View` command. - fn do_view_command(&mut self, command: ViewCommand) -> Result<()> { - // `View` commands depend on the number of dimensions. - G::do_view_command(self, command) + /// Executes a `DeleteSelection` command. + fn delete_selection(&mut self) { + if self.selection.is_some() { + self.record(); + self.selection.as_mut().unwrap().cells = None; + self.grab_selected_cells(); + self.selection.as_mut().unwrap().cells = None; + } } - /// Executes a `Draw` command. - fn do_draw_command(&mut self, command: DrawCommand) -> Result<()> { - // `Draw` commands depend on the number of dimensions. - G::do_draw_command(self, command) + + /// Executes a `Step` command. + fn step(&mut self, step_size: BigInt) -> Result<()> { + self.try_step(step_size)?; + Ok(()) } - /// Executes a `Select` command. - fn do_select_command(&mut self, command: SelectCommand) -> Result<()> { - // `Select` commands depend on the number of dimensions. - G::do_select_command(self, command) + /// Executes an `UpdateStepSize` command. + fn update_step_size(&mut self) -> Result<()> { + if self.is_running() { + self.stop_running(); + let restarted = self.start_running()?; // This should not fail. + if !restarted { + error!("Unable to restart simulation after updating step size"); + } + } + Ok(()) } - /// Starts a drag event. + /// Begins dragging. /// - /// Do not call this method from within a drag handler. - pub(super) fn start_drag(&mut self, drag_type: DragType, drag_handler: DragHandler) { - self.drag_type = Some(drag_type); - self.drag_handler = Some(drag_handler); + /// Do not call this method from within a drag update function. + pub(super) fn begin_drag(&mut self, command: DragCmd, pixel: FVec2D) { + if self.drag.is_some() { + warn!("Attempted to start new drag while still in the middle of one"); + } else { + let initial_screen_pos = self.screen_pos(pixel); + + let update_fn = match &command { + DragCmd::View(cmd) => self.make_drag_view_update_fn(*cmd, pixel), + + DragCmd::DrawFreeform(draw_mode) => { + self.make_drag_draw_update_fn(*draw_mode, initial_screen_pos) + } + + DragCmd::SelectNewRect => { + self.deselect(); + self.make_drag_select_new_rect_update_fn() + } + DragCmd::ResizeSelectionToCursor => { + self.make_drag_resize_selection_to_cursor_update_fn(initial_screen_pos) + } + DragCmd::ResizeSelection2D(direction) => { + ignore_command!(command, if D::NDIM != 2); + self.make_drag_resize_selection_update_fn( + initial_screen_pos, + direction.vector(), + ) + } + DragCmd::ResizeSelection3D(face) => { + ignore_command!(command, if D::NDIM != 3); + self.make_drag_resize_selection_update_fn( + initial_screen_pos, + face.normal_ivec(), + ) + } + DragCmd::MoveSelection(face) => { + self.make_drag_move_selection_update_fn(*face, initial_screen_pos, |this| { + // To move the selection, we must first drop the + // selected cells. + this.drop_selected_cells(); + }) + } + + DragCmd::MoveSelectedCells(face) => { + self.make_drag_move_selection_update_fn(*face, initial_screen_pos, |this| { + // To move the selected cells, we must first grab those + // cells. + this.grab_selected_cells(); + }) + } + + DragCmd::CopySelectedCells(face) => { + self.make_drag_move_selection_update_fn(*face, initial_screen_pos, |this| { + // To copy the selected cells, we must first grab a copy + // of those cells. If the cells are already grabbed, + // this does nothing. + this.grab_copy_of_selected_cells(); + }) + } + }; + + if update_fn.is_some() { + let cancel_fn: Option>; + + if CONFIG + .lock() + .hist + .should_record_history_for_drag_command(&command) + { + self.reset_worker_thread(); + self.record(); + + cancel_fn = Some(Box::new(|this| { + this.undo(); + })); + } else { + let old_ndtree = self.automaton.ndtree.clone(); + let old_selection = self.selection.clone(); + cancel_fn = Some(Box::new(move |this| { + this.automaton.ndtree = old_ndtree; + this.selection = old_selection; + })); + } + + // Recreate `initial_screen_pos` because it may have been + // consumed in the big `match`. + let initial_screen_pos = self.screen_pos(pixel); + let waiting_for_drag_threshold = command.always_uses_movement_threshold(); + self.drag = Some(Drag { + command, + initial_screen_pos, + waiting_for_drag_threshold, + + update_fn, + cancel_fn, + + ndtree_base_pos: self.automaton.ndtree.base_pos().clone(), + }); + } + } } /// Executes a `ContinueDrag` command, calling the drag handler. /// /// Do not call this method from within a drag handler. pub(super) fn continue_drag(&mut self, cursor_pos: FVec2D) -> Result<()> { - if let Some(mut drag_handler) = self.drag_handler.take() { - match drag_handler(self, cursor_pos)? { - DragOutcome::Continue => self.drag_handler = Some(drag_handler), - DragOutcome::Cancel => self.stop_drag(), + if let Some(mut drag) = self.drag.take() { + let screen_pos = self.screen_pos(cursor_pos); + let outcome = drag.update(self, &screen_pos)?; + self.drag = Some(drag); + match outcome { + DragOutcome::Continue => (), + DragOutcome::Cancel => self.end_drag(), } } Ok(()) } - /// Executes a `StopDrag` command. + /// Executes an `EndDrag` command. /// /// Do not call this method from within a drag handler; instead return /// `DragOutcome::Cancel`. - pub(super) fn stop_drag(&mut self) { - self.drag_type = None; - self.drag_handler = None; + pub(super) fn end_drag(&mut self) { + self.drag = None; + } + + fn make_drag_view_update_fn( + &self, + command: DragViewCmd, + cursor_start: FVec2D, + ) -> Option> { + self.viewpoint_interpolator + .make_drag_update_fn(command, cursor_start) + .map(|mut viewpoint_update_fn| -> DragUpdateFn { + Box::new(move |_drag, this, new_screen_pos| { + viewpoint_update_fn(&mut this.viewpoint_interpolator, new_screen_pos.pixel()) + }) + }) + } + fn make_drag_draw_update_fn( + &self, + draw_mode: DrawMode, + initial_pos: D::ScreenPos, + ) -> Option> { + let initial_cell = initial_pos + .op_pos_for_drag_command(&DragCmd::DrawFreeform(draw_mode))? + .into_cell(); + let new_cell_state = draw_mode.cell_state( + self.automaton.ndtree.get_cell(&initial_cell), + self.selected_cell_state, + ); + + let mut pos1 = initial_cell; + Some(Box::new(move |drag, this, new_screen_pos| { + if let Some(pos2) = drag.new_pos(&new_screen_pos).map(|p| p.into_cell()) { + for pos in bresenham::line(pos1.clone(), pos2.clone()) { + this.automaton.ndtree.set_cell(&pos, new_cell_state); + } + pos1 = pos2; + Ok(DragOutcome::Continue) + } else { + Ok(DragOutcome::Cancel) + } + })) + } + fn make_drag_select_new_rect_update_fn(&self) -> Option> { + Some(Box::new(move |drag, this, new_pos| { + if let Some(resize_start) = drag.initial_render_cell_rect() { + if let Some(resize_end) = drag.new_render_cell_rect(&new_pos) { + this.set_selection_rect(Some(NdRect::span_rects(&resize_start, &resize_end))); + } + Ok(DragOutcome::Continue) + } else { + Ok(DragOutcome::Cancel) + } + })) + } + fn make_drag_resize_selection_to_cursor_update_fn( + &self, + initial_pos: D::ScreenPos, + ) -> Option> { + let mut initial_selection = None; + let resize_start = initial_pos.absolute_selection_resize_start_pos()?; + Some(Box::new(move |drag, this, new_pos| { + initial_selection = initial_selection.take().or_else(|| this.deselect()); + if let Some(s) = &initial_selection { + if let Some(resize_end) = drag.new_render_cell_rect(&new_pos) { + this.set_selection_rect(Some(D::resize_selection_to_cursor( + &s.rect, + &resize_start, + &resize_end, + &drag, + ))); + } else { + this.set_selection_rect(Some(s.rect.clone())); + } + Ok(DragOutcome::Continue) + } else { + // There is no selection to resize. + Ok(DragOutcome::Cancel) + } + })) + } + fn make_drag_resize_selection_update_fn( + &self, + initial_pos: D::ScreenPos, + resize_vector: IVec, + ) -> Option> { + // Attempt to cast `resize_vector` to the correct number of dimensions. + let resize_vector: IVec = AnyDimIVec::from(resize_vector).try_into().ok()?; + + let mut initial_selection = None; + Some(Box::new(move |_drag, this, new_pos| { + initial_selection = initial_selection.take().or_else(|| this.deselect()); + if let Some(s) = &initial_selection { + if let Some(resize_delta) = + new_pos.rect_resize_delta(&s.rect.to_fixedrect(), &initial_pos, &resize_vector) + { + this.set_selection_rect(Some(super::selection::resize_selection_relative( + &s.rect, + &resize_delta, + &resize_vector, + ))); + } + Ok(DragOutcome::Continue) + } else { + // There is no selection to resize. + Ok(DragOutcome::Cancel) + } + })) + } + fn make_drag_move_selection_update_fn( + &self, + face: Option, + initial_pos: D::ScreenPos, + selection_setup_fn: impl 'static + Fn(&mut Self), + ) -> Option> { + let mut initial_selection = None; + Some(Box::new(move |_drag, this, new_pos| { + initial_selection = initial_selection.take().or_else(|| { + selection_setup_fn(this); + this.selection.take() + }); + if let Some(s) = &initial_selection { + if let Some(delta) = + new_pos.rect_move_delta(&s.rect.to_fixedrect(), &initial_pos, face) + { + this.selection = Some(s.move_by(delta.round())); + } + Ok(DragOutcome::Continue) + } else { + // There is no selection to move. + Ok(DragOutcome::Cancel) + } + })) } /// Submits a request to the worker thread. Returns an error if the worker @@ -416,12 +798,14 @@ impl GenericGridView { } } /// Starts continuous simulation; does nothing if it is already running. - /// Returns an error if unsuccessful (e.g. the user is drawing). - fn start_running(&mut self) -> Result<()> { + /// Returns `true` if the simulation is now running, or false if another + /// running operation prevented starting it (e.g. drawing or some other + /// background task). + fn start_running(&mut self) -> Result { if self.is_running() { - return Ok(()); + return Ok(true); } else if self.is_drawing() { - return Err(anyhow!("Cannot start simulation while drawing")); + return Ok(false); } self.reset_worker_thread(); @@ -446,7 +830,17 @@ impl GenericGridView { }), )?; self.record(); - Ok(()) + Ok(true) + } + /// Toggles continuous simulation. Returns `true` if the simulation is now + /// running, or `false` if it is not. The operation may not succeed. + fn toggle_running(&mut self) -> Result { + if self.is_running() { + self.stop_running(); + Ok(false) // `stop_running()` always succeeds. + } else { + self.start_running() + } } /// Returns whether the simulation is running (i.e. stepping forward every /// frame automatically with no user input). @@ -508,31 +902,35 @@ impl GenericGridView { /// Sets the selection, or returns an error if the dimensionality of the /// selection does not match the dimensionality of the gridview. - fn set_selection_nd(&mut self, new_selection: Option>) -> Result<()> { - if G::D::NDIM == D::NDIM { - self.set_selection(unsafe { - *std::mem::transmute::>>, Box>>>( - Box::new(new_selection), - ) - }); - Ok(()) - } else { - Err(anyhow!( - "set_selection_nd() received Selection of wrong dimensionality" + fn set_selection_nd(&mut self, new_selection: Option>) -> Result<()> { + ensure!( + D::NDIM == D2::NDIM, + "set_selection_nd() received Selection of wrong dimensionality", + ); + + self.set_selection(unsafe { + *std::mem::transmute::>>, Box>>>(Box::new( + new_selection, )) - } + }); + Ok(()) } /// Deselects and sets a new selection. - pub(super) fn set_selection(&mut self, new_selection: Option>) { + pub(super) fn set_selection(&mut self, new_selection: Option>) { self.deselect(); self.selection = new_selection; } /// Deselects and sets a new selection rectangle. - pub(super) fn set_selection_rect(&mut self, new_selection_rect: Option>) { + pub(super) fn set_selection_rect(&mut self, new_selection_rect: Option>) { self.set_selection(new_selection_rect.map(Selection::from)) } + /// Selects all cells in the pattern. + pub(super) fn select_all(&mut self) { + self.deselect(); // Include pasted cells. + self.set_selection(self.automaton.ndtree.bounding_rect().map(Selection::from)); + } /// Returns the selection rectangle. - pub(super) fn selection_rect(&self) -> Option> { + pub(super) fn selection_rect(&self) -> Option> { if let Some(s) = &self.selection { Some(s.rect.clone()) } else { @@ -540,7 +938,7 @@ impl GenericGridView { } } /// Deselects all and returns the old selection. - pub(super) fn deselect(&mut self) -> Option> { + pub(super) fn deselect(&mut self) -> Option> { if let Some(sel) = self.selection.clone() { if let Some(cells) = sel.cells { self.record(); @@ -563,6 +961,42 @@ impl GenericGridView { } self.selection.take() } + /// Moves the selection to the center of the screen along each axis for + /// which it is outside the viewport. + fn ensure_selection_visible(&mut self) { + if let Some(mut sel) = self.selection.take() { + // The number of render cells of padding to ensure. + const PADDING: usize = 2; + + let render_cell_layer = self.viewpoint().render_cell_layer(); + + // Convert to render cells. + let sel_rect = sel.rect.div_outward(&render_cell_layer.big_len()); + let visible_rect = self + .viewpoint() + .global_visible_rect() + .div_outward(&render_cell_layer.big_len()); + + let sel_min = sel_rect.min(); + let sel_max = sel_rect.max(); + let sel_center = sel_rect.center(); + let visible_min = visible_rect.min(); + let visible_max = visible_rect.max(); + let view_center = self.viewpoint().center().floor(); + + for &ax in Dim2D::axes() { + if sel_max[ax] < visible_min[ax].clone() + PADDING + || visible_max[ax] < sel_min[ax].clone() + PADDING + { + // Move selection to center along this axis. + sel = + sel.move_by(NdVec::unit(ax) * (view_center[ax].clone() - &sel_center[ax])); + } + } + + self.set_selection(Some(sel)); + } + } /// Moves the selected cells from the selection to the main grid. Outputs a /// warning in the log if there is no selection. pub(super) fn drop_selected_cells(&mut self) { @@ -598,18 +1032,21 @@ impl GenericGridView { } } - /// Returns whether the user is currently drawing. - pub fn is_drawing(&self) -> bool { - self.drag_type == Some(DragType::Drawing) - } - /// Returns whether the user is currently moving the viewpoint by dragging - /// the mouse. - pub fn is_dragging_view(&self) -> bool { - self.drag_type == Some(DragType::MovingView) - } /// Returns whether the user is currently dragging the mouse. pub fn is_dragging(&self) -> bool { - self.drag_type.is_some() + self.drag.is_some() + } + /// Returns the current mouse drag. + pub fn get_drag(&self) -> Option<&Drag> { + self.drag.as_ref() + } + /// Returns whether the user is currently drawing. + pub fn is_drawing(&self) -> bool { + if let Some(drag) = &self.drag { + drag.command.is_draw_cmd() + } else { + false + } } /// Returns the type of operation currently happening in the background. @@ -672,9 +1109,13 @@ impl GenericGridView { } /// Returns the current viewpoint. - pub fn viewpoint(&self) -> &G::Viewpoint { + pub fn viewpoint(&self) -> &D::Viewpoint { &self.viewpoint_interpolator.current } + /// Returns a mutable reference to the target viewpoint. + pub fn target_viewpoint(&mut self) -> &mut D::Viewpoint { + &mut self.viewpoint_interpolator.target + } /// Updates viewpoint parameters and renders the gridview, recording and /// returning the result. pub fn render(&mut self, params: RenderParams<'_>) -> Result<&RenderResult> { @@ -686,13 +1127,38 @@ impl GenericGridView { .set_target_dimensions(params.target.get_dimensions()); // Render the grid to the target, then save and return the result. - self.last_render_result = G::render(self, params)?; + self.last_render_result = D::render(self, params)?; Ok(self.last_render_result()) } /// Returns data generated by the most recent render. pub fn last_render_result(&self) -> &RenderResult { &self.last_render_result } + /// Returns a useful representation of a pixel position on the screen. + pub fn screen_pos(&self, pixel: FVec2D) -> D::ScreenPos { + D::screen_pos(self, pixel) + } + /// Returns the cell to highlight for a given mouse display mode. + pub(super) fn cell_to_highlight(&self, mouse: &MouseState) -> Option> { + let screen_pos = self.screen_pos(mouse.pos?); + if let Some(drag) = self.get_drag() { + drag.new_pos(&screen_pos) + } else { + screen_pos.op_pos_for_mouse_display_mode(mouse.display_mode) + } + } + /// Returns the rectangle of the render cell to highlight for a given mouse + /// display mode. + pub(super) fn render_cell_rect_to_highlight(&self, mouse: &MouseState) -> Option> { + Some( + self.viewpoint() + .render_cell_layer() + .round_rect_with_base_pos( + BigRect::single_cell(self.cell_to_highlight(mouse)?.into_cell()), + self.automaton.ndtree.base_pos(), + ), + ) + } /// Returns the time duration measured between the last two frames, or /// `None` if there is not enough data. @@ -705,24 +1171,35 @@ impl GenericGridView { } } -pub trait GridViewDimension: fmt::Debug + Default { - type D: Dim; - type Viewpoint: Viewpoint; +pub trait GridViewDimension: Dim { + type Viewpoint: Viewpoint; + type ScreenPos: ScreenPosTrait; - /// Executes a `View` command. - fn do_view_command(this: &mut GenericGridView, command: ViewCommand) -> Result<()>; - /// Executes a `Draw` command. - fn do_draw_command(this: &mut GenericGridView, command: DrawCommand) -> Result<()>; - /// Executes a `Select` command. - fn do_select_command(this: &mut GenericGridView, command: SelectCommand) -> Result<()>; + /// Extra data stored for a `GridView` with this number of dimensions. + type Data: fmt::Debug + Default; + + /// Executes a `FocusCursor` command. This has different behavior in 2D vs. + /// 3D. + fn focus(this: &mut GenericGridView, pos: &Self::ScreenPos); + /// Resizes a selection rectangle to the cell at the cursor. This has + /// different behavior in 2D vs. 3D. + fn resize_selection_to_cursor( + rect: &BigRect, + start: &FixedVec, + end: &BigRect, + drag: &Drag, + ) -> BigRect; /// Renders the gridview. fn render(this: &mut GenericGridView, params: RenderParams<'_>) -> Result; + + /// Returns a useful representation of a pixel position on the screen. + fn screen_pos(this: &GenericGridView, pixel: FVec2D) -> Self::ScreenPos; } #[derive(Debug, Clone)] -pub struct HistoryEntry { - automaton: NdAutomaton, - selection: Option>, - viewpoint: G::Viewpoint, +pub struct HistoryEntry { + automaton: NdAutomaton, + selection: Option>, + viewpoint: D::Viewpoint, } diff --git a/ui/src/gridview/history.rs b/ui/src/gridview/history.rs index 35c3c731..658d10c3 100644 --- a/ui/src/gridview/history.rs +++ b/ui/src/gridview/history.rs @@ -119,10 +119,6 @@ where mod tests { use super::*; - fn replace<'a, T>(state: &'a mut T) -> impl 'a + FnMut(T) -> T { - move |x| std::mem::replace(state, x) - } - struct HistoryTester { /// Current value. curr: i32, diff --git a/ui/src/gridview/mod.rs b/ui/src/gridview/mod.rs index 8ee6be01..ace04965 100644 --- a/ui/src/gridview/mod.rs +++ b/ui/src/gridview/mod.rs @@ -1,37 +1,33 @@ use anyhow::Result; use enum_dispatch::enum_dispatch; use std::collections::VecDeque; +use std::sync::Arc; use std::time::Duration; use ndcell_core::prelude::*; +mod algorithms; +mod drag; mod generic; mod history; pub mod render; +mod screenpos; mod selection; mod view2d; mod view3d; mod viewpoint; mod worker; -use crate::commands::*; +use crate::commands::{CmdMsg, DragCmd}; pub use generic::GenericGridView; pub use history::History; pub use render::{MouseTargetData, RenderParams, RenderResult}; +pub use screenpos::*; pub use selection::*; -pub use view2d::{GridView2D, ScreenPos2D}; +pub use view2d::GridView2D; pub use view3d::GridView3D; pub use viewpoint::*; -/// Handler for mouse drag events, when the user starts dragging and called for -/// each cursor movement until released. Returns whether to continue or cancel -/// the drag. -/// -/// It takes as input the current state, which is mutated, and the current mouse -/// cursor position. If any initial state or mouse cursor history is relevant, -/// the closure must maintain this information. -pub type DragHandler = Box Result>; - /// Abstraction over 2D and 3D gridviews. #[enum_dispatch(History)] pub enum GridView { @@ -80,6 +76,13 @@ impl GridView { } } + pub fn rule(&self) -> Rule { + match self { + GridView::View2D(view2d) => Rule::Rule2D(Arc::clone(&view2d.automaton.rule)), + GridView::View3D(view3d) => Rule::Rule3D(Arc::clone(&view3d.automaton.rule)), + } + } + pub fn selected_cell_state(&self) -> u8 { match self { GridView::View2D(view2d) => view2d.selected_cell_state, @@ -112,16 +115,22 @@ impl GridView { GridView::View3D(view3d) => view3d.work_type(), } } - pub fn is_drawing(&self) -> bool { + pub fn is_dragging(&self) -> bool { match self { - GridView::View2D(view2d) => view2d.is_drawing(), - GridView::View3D(view3d) => view3d.is_drawing(), + GridView::View2D(view2d) => view2d.is_dragging(), + GridView::View3D(view3d) => view3d.is_dragging(), } } - pub fn is_dragging_view(&self) -> bool { + pub fn drag_cmd(&self) -> Option<&DragCmd> { match self { - GridView::View2D(view2d) => view2d.is_dragging_view(), - GridView::View3D(view3d) => view3d.is_dragging_view(), + GridView::View2D(view2d) => view2d.get_drag().map(|d| &d.command), + GridView::View3D(view3d) => view3d.get_drag().map(|d| &d.command), + } + } + pub fn is_drawing(&self) -> bool { + match self { + GridView::View2D(view2d) => view2d.is_drawing(), + GridView::View3D(view3d) => view3d.is_drawing(), } } pub fn is_running(&self) -> bool { @@ -132,7 +141,7 @@ impl GridView { } /// Enqueues a command to be executed on the next frame. - pub fn enqueue(&self, command: impl Into) { + pub fn enqueue(&self, command: impl Into) { match self { GridView::View2D(view2d) => view2d.enqueue(command), GridView::View3D(view3d) => view3d.enqueue(command), @@ -154,6 +163,20 @@ impl GridView { GridView::View3D(view3d) => view3d.render(params), } } + /// Exports the simulation to a string. + pub fn export(&self, format: CaFormat) -> Result { + match self { + GridView::View2D(view2d) => view2d.export(format), + GridView::View3D(view3d) => view3d.export(format), + } + } + /// Returns whether there is a selection. + pub fn has_selection(&self) -> bool { + match self { + GridView::View2D(view2d) => view2d.selection.is_some(), + GridView::View3D(view3d) => view3d.selection.is_some(), + } + } } #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -161,16 +184,3 @@ pub enum WorkType { SimStep, SimContinuous, } - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum DragType { - MovingView, - Drawing, - Selecting, -} - -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum DragOutcome { - Continue, - Cancel, -} diff --git a/ui/src/gridview/render/generic.rs b/ui/src/gridview/render/generic.rs index 08f5c0e6..de29e555 100644 --- a/ui/src/gridview/render/generic.rs +++ b/ui/src/gridview/render/generic.rs @@ -2,6 +2,7 @@ use anyhow::{Context, Result}; use glium::glutin::event::ModifiersState; use glium::index::PrimitiveType; use glium::{uniform, Surface}; +use palette::{Mix, Srgb, Srgba}; use std::cell::RefMut; use ndcell_core::prelude::*; @@ -13,11 +14,12 @@ use super::vertices::MouseTargetVertex; use super::CellDrawParams; use crate::ext::*; use crate::gridview::*; +use crate::Face; pub struct GenericGridViewRender<'a, R: GridViewRenderDimension<'a>> { pub(super) cache: RefMut<'a, super::RenderCache>, pub(super) params: RenderParams<'a>, - pub(super) dim: R, + pub(super) dim: Option, /// Viewpoint to render the grid from. pub(super) viewpoint: &'a R::Viewpoint, @@ -34,6 +36,9 @@ pub struct GenericGridViewRender<'a, R: GridViewRenderDimension<'a>> { mouse_targets: Vec, /// Vertex data for mouse targets. mouse_target_tris: Vec, + + /// Overlay rectangles to draw batched for performance and transparency. + pub(super) overlay_quads: Vec, } impl<'a, R: GridViewRenderDimension<'a>> GenericGridViewRender<'a, R> { /// Creates a `GridViewRender` for a gridview. @@ -41,9 +46,11 @@ impl<'a, R: GridViewRenderDimension<'a>> GenericGridViewRender<'a, R> { let mut cache = super::CACHE.borrow_mut(); // Initialize color and depth buffers. - params - .target - .clear_color_srgb_and_depth(R::DEFAULT_COLOR, R::DEFAULT_DEPTH); + let color = R::DEFAULT_COLOR; + params.target.clear_color_srgb_and_depth( + (color.red, color.green, color.blue, 1.0), + R::DEFAULT_DEPTH, + ); // Initialize mouse picker. cache.picker.init(params.target.get_dimensions()); @@ -59,10 +66,10 @@ impl<'a, R: GridViewRenderDimension<'a>> GenericGridViewRender<'a, R> { .global_to_local_int_rect(&global_visible_rect) .expect("Unreasonable visible rectangle"); - R::init(Self { + let mut ret = Self { cache, params, - dim: R::default(), + dim: None, viewpoint, xform, @@ -71,11 +78,16 @@ impl<'a, R: GridViewRenderDimension<'a>> GenericGridViewRender<'a, R> { mouse_targets: vec![], mouse_target_tris: vec![], - }) + + overlay_quads: vec![], + }; + ret.dim = Some(R::init(&ret)); + ret } /// Returns a `RenderResult` from this render. pub fn finish(mut self) -> Result { + R::draw_overlay_quads(&mut self).context("Drawing overlay")?; Ok(RenderResult { mouse_target: self.render_mouse_targets()?, }) @@ -103,7 +115,8 @@ impl<'a, R: GridViewRenderDimension<'a>> GenericGridViewRender<'a, R> { &uniform! { matrix: self.xform.gl_matrix() }, &glium::DrawParameters { depth: glium::Depth { - test: glium::DepthTest::Overwrite, + test: glium::DepthTest::IfLessOrEqual, + write: true, ..Default::default() }, viewport: Some(picker_viewport), @@ -126,41 +139,27 @@ impl<'a, R: GridViewRenderDimension<'a>> GenericGridViewRender<'a, R> { .and_then(|i| self.mouse_targets.get(i.checked_sub(1)?)) .cloned()) } - pub(super) fn add_mouse_target_quad( - &mut self, - modifiers: ModifiersState, - cells: FRect2D, - data: MouseTargetData, - ) { + pub(super) fn add_mouse_target(&mut self, data: MouseTargetData) -> u32 { self.mouse_targets.push(data); - let target_id = self.mouse_targets.len() as u32; // IDs start at 1 - let NdVec([x1, y1]) = cells.min(); - let NdVec([x2, y2]) = cells.max(); - let corners = [ - NdVec([x1, y1]), - NdVec([x2, y1]), - NdVec([x1, y2]), - NdVec([x2, y2]), - ]; - self.add_mouse_target_tri(modifiers, [corners[0], corners[1], corners[2]], target_id); - self.add_mouse_target_tri(modifiers, [corners[3], corners[2], corners[1]], target_id); + self.mouse_targets.len() as u32 // IDs start at 1 } - fn add_mouse_target_tri( + pub(super) fn add_mouse_target_tri( &mut self, - modifiers: ModifiersState, - points: [FVec2D; 3], + modifiers: Option, + points: [FVec3D; 3], target_id: u32, ) { - if self.params.modifiers == modifiers { - let z = 0.0; - for &point in &points { - let [x, y] = point.to_f32_array(); - self.mouse_target_tris.push(MouseTargetVertex { - pos: [x, y, z], - target_id, - }) + if let Some(mods) = modifiers { + if self.params.modifiers != mods { + return; } } + + for &point in &points { + let pos = point.to_f32_array(); + self.mouse_target_tris + .push(MouseTargetVertex { pos, target_id }) + } } /// Converts a global position to a local position, clamping it to the @@ -230,59 +229,192 @@ impl<'a, R: GridViewRenderDimension<'a>> GenericGridViewRender<'a, R> { .unwrap_or(0) } - /// Returns the color to represent an ND-tree node. - pub(super) fn ndtree_node_color(node: NodeRef<'_, R::D>) -> [u8; 4] { - if let Some(cell_state) = node.single_state() { - match cell_state { - 0_u8 => crate::colors::DEAD, - 1_u8 => crate::colors::LIVE, - i => { - let [r, g, b] = colorous::TURBO - .eval_rational(257 - i as usize, 256) - .as_array(); - [r, g, b, 255] - } - } - } else { - let ratio = if node.is_empty() { - 0.0 - } else { - // Multiply then divide by 255 to keep some precision. - let population_ratio = (node.population() * 255_usize / node.big_num_cells()) - .to_f64() - .unwrap() - / 255.0; - // Bias so that 50% is the minimum brightness if there are any - // live cells. - (population_ratio / 2.0) + 0.5 + /// Returns the endpoint pairs for a single crosshair. + pub(super) fn make_crosshair_endpoints( + &mut self, + parallel_axis: Axis, + a: R64, + b: R64, + position: FVec, + color: Srgb, + ) -> Vec<[LineEndpoint; 2]> + where + FVec: Copy, + { + let bright_color = color; + let dull_color = Srgb::from_linear(Mix::mix( + &R::DEFAULT_COLOR.into_linear(), + &color.into_linear(), + crate::colors::CROSSHAIR_OPACITY, + )); + + let gradient_len = std::cmp::max( + r64(CROSSHAIR_GRADIENT_MIN_PIXEL_LEN) * self.xform.render_cell_scale.cells_per_unit(), + r64(CROSSHAIR_GRADIENT_MIN_CELL_LEN), + ); + + let (pos1, pos2, pos3, pos4, pos5, pos6); + { + let visible_rect = self.local_visible_rect.to_frect(); + let pos_along_line = |coord| { + let mut ret = position; + ret[parallel_axis] = coord; + ret }; + pos1 = pos_along_line(visible_rect.min()[parallel_axis]); + pos2 = pos_along_line(a - gradient_len); + pos3 = pos_along_line(a); + pos4 = pos_along_line(b); + pos5 = pos_along_line(b + gradient_len); + pos6 = pos_along_line(visible_rect.max()[parallel_axis]); + } + + vec![ + [ + LineEndpoint::include(pos1, dull_color), + LineEndpoint::include(pos2, dull_color), + ], + [ + LineEndpoint::exclude(pos2, dull_color), + LineEndpoint::exclude(pos3, bright_color), + ], + [ + LineEndpoint::include(pos3, bright_color), + LineEndpoint::include(pos4, bright_color), + ], + [ + LineEndpoint::exclude(pos4, bright_color), + LineEndpoint::exclude(pos5, dull_color), + ], + [ + LineEndpoint::include(pos5, dull_color), + LineEndpoint::include(pos6, dull_color), + ], + ] + } + /// Returns an `FRect` for the line, swapping the endpoints if necessary so + /// that `start[line_axis] < end[line_axis]`. + pub(super) fn make_line_ndrect( + &mut self, + start: &mut LineEndpoint, + end: &mut LineEndpoint, + width: R64, + ) -> (FRect, Axis) + where + FVec: Copy, + { + let min_width = self.xform.render_cell_scale.cells_per_unit() * R::LINE_MIN_PIXEL_WIDTH; + let width = if self.xform.render_cell_layer == Layer(0) { + std::cmp::max(width, min_width) + } else { + min_width + }; + + let rect = FRect::span(start.pos, end.pos); + let axis = rect.size().max_axis(); + if start.pos[axis] > end.pos[axis] { + std::mem::swap(start, end); + } - // Set alpha to live:dead ratio. - let mut color = crate::colors::LIVE; - color[3] = (color[3] as f64 * ratio) as u8; - color + let mut min_offset = NdVec::repeat(-width / 2.0); + let mut max_offset = NdVec::repeat(width / 2.0); + if !start.include_endpoint { + min_offset[axis] *= -1.0; } + if !end.include_endpoint { + max_offset[axis] *= -1.0; + } + (rect.offset_min_max(min_offset, max_offset), axis) } } -pub trait GridViewRenderDimension<'a>: Default { +pub trait GridViewRenderDimension<'a>: Sized { type D: Dim; type Viewpoint: Viewpoint; + type OverlayQuad: Copy; - const DEFAULT_COLOR: (f32, f32, f32, f32); + const DEFAULT_COLOR: Srgb; const DEFAULT_DEPTH: f32; + const LINE_MIN_PIXEL_WIDTH: f64; + + fn init(gvr: &GenericGridViewRender<'a, Self>) -> Self; + + fn draw_overlay_quads(this: &mut GenericGridViewRender<'a, Self>) -> Result<()>; +} + +pub(super) type LineEndpoint2D = LineEndpoint; +pub(super) type LineEndpoint3D = LineEndpoint; - fn init(this: GenericGridViewRender<'a, Self>) -> GenericGridViewRender<'a, Self> { - this +#[derive(Debug, Clone)] +pub(super) struct LineEndpoint { + pub pos: FVec, + pub color: Srgba, + pub include_endpoint: bool, +} +impl Copy for LineEndpoint where FVec: Copy {} +impl LineEndpoint { + pub fn include(pos: FVec, color: impl Into) -> Self { + Self { + pos, + color: color.into(), + include_endpoint: true, + } + } + pub fn exclude(pos: FVec, color: impl Into) -> Self { + Self { + pos, + color: color.into(), + include_endpoint: false, + } } } +/// Fill style for an overlay quad. #[derive(Debug, Copy, Clone)] -pub(super) struct LineParams { - /// Line width. - pub width: f64, - /// Whether to include the squares at the endpoints of this line. - pub include_endpoints: bool, - /// The axis this line is along. - pub axis: Axis, +pub(super) enum OverlayFill { + Solid(Srgba), + Gradient(Axis, Srgba, Srgba), + Gridlines3D, +} +impl From for OverlayFill { + fn from(color: Srgba) -> Self { + Self::Solid(color) + } +} +impl OverlayFill { + pub fn gradient(axis: Axis, color1: Srgba, color2: Srgba) -> Self { + if color1 == color2 { + Self::Solid(color1) + } else { + Self::Gradient(axis, color1, color2) + } + } + pub fn vertex_colors(self, face: Face) -> [Srgba; 4] { + match self { + OverlayFill::Gridlines3D => [Srgba::new(0.0, 0.0, 0.0, 0.0); 4], // ignored in vertex shader + OverlayFill::Solid(color) => [color; 4], + OverlayFill::Gradient(gradient_axis, c1, c2) => { + let [ax1, ax2] = face.plane_axes(); + if gradient_axis == ax1 { + [c1, c2, c1, c2] + } else if gradient_axis == ax2 { + [c1, c1, c2, c2] + } else { + match face.sign() { + Sign::Minus => [c1; 4], + Sign::NoSign => unreachable!(), + Sign::Plus => [c2; 4], + } + } + } + } + } + /// Returns `true` if the quad is definitely 100% opaque. + pub fn is_opaque(self) -> bool { + match self { + Self::Solid(color) => color.alpha >= 1.0, + Self::Gradient(_, c1, c2) => c1.alpha >= 1.0 && c2.alpha >= 1.0, + Self::Gridlines3D => false, + } + } } diff --git a/ui/src/gridview/render/gl_ndtree.rs b/ui/src/gridview/render/gl_ndtree.rs index 3b627af4..944a4086 100644 --- a/ui/src/gridview/render/gl_ndtree.rs +++ b/ui/src/gridview/render/gl_ndtree.rs @@ -5,13 +5,14 @@ use glium::texture::unsigned_texture2d::UnsignedTexture2d; use glium::texture::{ClientFormat, RawImage2d}; use itertools::Itertools; use log::warn; +use palette::{Mix, Pixel, Srgba}; use std::borrow::Cow; use std::collections::HashMap; use std::sync::Once; use ndcell_core::prelude::*; -use crate::DISPLAY; +use crate::{CONFIG, DISPLAY}; /// Texture size threshold beyond which to write a warning to the log. /// @@ -36,12 +37,7 @@ pub struct GlNdTreeCache { unused: HashMap<(ArcNode, Layer), GlNdTree>, } impl GlNdTreeCache { - pub fn gl_ndtree_from_node( - &mut self, - node: ArcNode, - min_layer: Layer, - pixelator: impl FnMut(NodeRef<'_, D>) -> [u8; 4], - ) -> Result<&GlNdTree> { + pub fn gl_ndtree_from_node(&mut self, node: ArcNode, min_layer: Layer) -> Result<&GlNdTree> { let key = (node, min_layer); // There's some unnecessary mutation of the `HashMap` here, but this @@ -56,7 +52,7 @@ impl GlNdTreeCache { } else { // We DO need to regenerate the texture. let node_ref = key.0.as_ref_with_guard(); - GlNdTree::from_node(&node_ref, min_layer, pixelator)? + GlNdTree::from_node(&node_ref, min_layer)? }; Ok(self.used.entry(key).or_insert(ret)) } @@ -82,11 +78,7 @@ pub struct GlNdTree { impl GlNdTree { /// Constructs a `GlNdTree` from a node and a function to turn a node into a /// solid color. - pub fn from_node<'n, N: NodeRefTrait<'n>>( - node: N, - min_layer: Layer, - mut pixelator: impl FnMut(NodeRef<'n, N::D>) -> [u8; 4], - ) -> Result { + pub fn from_node<'n, N: NodeRefTrait<'n>>(node: N, min_layer: Layer) -> Result { // Use the parent layer because we want to store four pixels (each // representing a node at `min_layer`) inside one index. let flat_ndtree = FlatNdTree::from_node(node, min_layer.parent_layer(), |node| node); @@ -100,8 +92,9 @@ impl GlNdTree { .subdivide() .unwrap() .into_iter() - .map(&mut pixelator) - .map(u32::from_be_bytes) // shader expects big-endian + .map(ndtree_node_color) // `NodeRef` to `Srgba` + .map(|color| color.into_format::().into_raw()) // `Srgba` to `[u8; 4]` + .map(u32::from_be_bytes) // `[u8; 4]` to `u32` (shader expects big-endian) .collect_vec(), FlatNdTreeNode::NonLeaf(indices, _) => { indices.into_iter().map(|&i| i as u32).collect_vec() @@ -143,3 +136,26 @@ impl GlNdTree { }) } } + +/// Returns the color to represent an ND-tree node. +pub fn ndtree_node_color(node: NodeRef<'_, D>) -> Srgba { + if let Some(cell_state) = node.single_state() { + CONFIG.lock().gfx.cell_colors[cell_state as usize] // TODO: is locking the mutex a perf issue? + } else { + // Multiply then divide by 255 to keep some precision. + let population_ratio = (node.population() * 255_usize / node.big_num_cells()) + .to_f32() + .unwrap() + / 255.0; + // Bias so that 50% is the minimum brightness if there are any + // live cells. + let mix_factor = (population_ratio / 2.0) + 0.5; + + // Mix colors for state #0 and #1 using proportion of live cells. + Srgba::from_linear(Mix::mix( + &crate::colors::cells::DEAD.into_linear(), + &crate::colors::cells::LIVE.into_linear(), + mix_factor, + )) + } +} diff --git a/ui/src/gridview/render/mod.rs b/ui/src/gridview/render/mod.rs index 5b4ad7a5..840ea327 100644 --- a/ui/src/gridview/render/mod.rs +++ b/ui/src/gridview/render/mod.rs @@ -6,8 +6,8 @@ use std::cell::RefCell; use ndcell_core::prelude::*; -use crate::config::MouseDragBinding; -use crate::mouse::{MouseDisplay, MouseState}; +use crate::commands::DragCmd; +use crate::mouse::MouseState; mod generic; mod gl_ndtree; @@ -25,14 +25,29 @@ pub(super) use render2d::GridViewRender2D; pub(super) use render3d::GridViewRender3D; mod consts { + /// Minimum pixel width of 2D lines. + pub const LINE_MIN_PIXEL_WIDTH_2D: f64 = 1.0; + /// Minimum pixel width of 3D lines. + pub const LINE_MIN_PIXEL_WIDTH_3D: f64 = 0.75; + + /// Minimum pixel length of the crosshair gradient. + pub const CROSSHAIR_GRADIENT_MIN_PIXEL_LEN: f64 = 16.0; + /// Minimum render cell length of the crosshair gradient. + pub const CROSSHAIR_GRADIENT_MIN_CELL_LEN: f64 = 1.0; + + /// Small value for avoiding Z-fighting. + pub const Z_EPSILON: f64 = 1.0 / 256.0; + /// Padding added to integer cuboid overlays, measured in render cells. + pub const CUBOID_OVERLAY_PADDING: f64 = GRIDLINE_WIDTH / 2.0; + /// Width of gridlines, measured in cells. pub const GRIDLINE_WIDTH: f64 = 1.0 / 32.0; /// Width of hover outline, measured in cells. pub const HOVER_HIGHLIGHT_WIDTH: f64 = 2.0 * GRIDLINE_WIDTH; /// Width of selection outline, measured in cells. pub const SELECTION_HIGHLIGHT_WIDTH: f64 = 4.0 * GRIDLINE_WIDTH; - /// Width of selection resize preview outline, measured in cells. - pub const SELECTION_RESIZE_PREVIEW_WIDTH: f64 = 2.0 * GRIDLINE_WIDTH; + /// Multiplicative fudge factor to prevent Z-fighting on outlines. + pub const WIDTH_FUDGE_FACTOR_3D: f64 = 1.125; /// Coefficient to use for gridline spacing. /// @@ -63,28 +78,15 @@ mod consts { /// Number of mouse target rectangles in each render batch. pub const MOUSE_TARGET_BATCH_SIZE: usize = 256; - /// Depth at which to render gridlines. - pub const GRIDLINE_DEPTH: f32 = 0.1; - /// Depth at which to render highlight/crosshairs. - pub const CURSOR_DEPTH: f32 = 0.2; - /// Depth at which to render selection rectangle. - pub const SELECTION_DEPTH: f32 = 0.3; - /// Depth at which to render selection resize preview. - pub const SELECTION_RESIZE_DEPTH: f32 = 0.4; - /// Direction that 3D light comes from (normalized in GLSL). pub const LIGHT_DIRECTION: [f32; 3] = [1.0, 7.0, -3.0]; /// Proportion of 3D light that is ambient, as opposed to directional. pub const LIGHT_AMBIENTNESS: f32 = 0.4; - /// Maximum 3D light level. - pub const MAX_LIGHT: f32 = 1.0; + /// Constant 3D lighting multiplier. + pub const LIGHT_MULTIPLIER: f32 = 1.0; /// Proportional radius of the visible area beyond which there is fog. pub const FOG_START_FACTOR: f32 = 0.5; - - /// Small offset used to force correct Z order or align things at the - /// sub-pixel scale. - pub const TINY_OFFSET: f32 = 1.0 / 16.0; } lazy_static! { @@ -116,17 +118,16 @@ pub(super) struct CellDrawParams<'a, D: Dim> { pub rect: Option<&'a BigRect>, /// Alpha value for the whole ND-tree. pub alpha: f32, + /// Whether these cells can be interacted with. + pub interactive: bool, } /// How to handle a mouse hover or click on a particular location on the screen. -#[derive(Debug, Default, Clone)] +#[derive(Debug, Clone)] pub struct MouseTargetData { /// Mouse binding for clicking the left mouse button over the target and /// dragging. - pub binding: Option, - /// Display mode for the cursor when hovering over the target or clicking on - /// it and dragging. - pub display: MouseDisplay, + pub binding: DragCmd, } #[derive(Default)] @@ -144,6 +145,12 @@ pub fn post_frame_clean_cache() { cache.gl_quadtrees.post_frame_clean_cache(); } +pub fn invalidate_gl_ndtree_cache() { + let mut cache = CACHE.borrow_mut(); + cache.gl_quadtrees.invalidate_all(); + cache.gl_octrees.invalidate_all(); +} + pub fn hot_reload_shaders() { shaders::hot_reload_all(); } diff --git a/ui/src/gridview/render/picker.rs b/ui/src/gridview/render/picker.rs index f2bf494c..405b7ef6 100644 --- a/ui/src/gridview/render/picker.rs +++ b/ui/src/gridview/render/picker.rs @@ -1,7 +1,6 @@ //! OpenGL pixel buffer objects to detect what the mouse is hovering over. use glium::framebuffer::{DepthRenderBuffer, SimpleFrameBuffer}; -use glium::texture::pixel_buffer::PixelBuffer; use glium::texture::{DepthFormat, MipmapsOption, UncompressedUintFormat, UnsignedTexture2d}; use glium::Surface; @@ -10,13 +9,11 @@ use crate::DISPLAY; /// Pixel buffer object that tells which target the cursor is hovering over. pub struct MousePicker { - pbo: PixelBuffer, attachments: Resizing<(UnsignedTexture2d, DepthRenderBuffer)>, } impl Default for MousePicker { fn default() -> Self { Self { - pbo: PixelBuffer::new_empty(&**DISPLAY, 1), attachments: Resizing::with_generator(|w, h| { let texture = UnsignedTexture2d::empty_with_format( &**DISPLAY, @@ -39,7 +36,7 @@ impl MousePicker { pub fn init(&mut self, (target_w, target_h): (u32, u32)) { self.attachments.set_min_size(target_w, target_h); let (mut fbo, _viewport) = self.fbo(); - fbo.clear_color_and_depth((0.0, 0.0, 0.0, 0.0), 0.0); + fbo.clear_color_and_depth((0.0, 0.0, 0.0, 0.0), f32::INFINITY); } pub fn fbo<'a>(&'a mut self) -> (SimpleFrameBuffer<'a>, glium::Rect) { @@ -58,26 +55,34 @@ impl MousePicker { (fbo, viewport) } - pub fn get_pixel(&self, (cursor_x, cursor_y): (u32, u32)) -> u32 { - let (target_w, target_h) = self.attachments.desired_size().unwrap(); - let (texture, _depth) = self.attachments.unwrap(); - - let single_pixel_rect = glium::Rect { - left: cursor_x, - bottom: target_h.saturating_sub(cursor_y + 1), - width: 1, - height: 1, - }; + pub fn get_pixel(&self, cursor_pos: (u32, u32)) -> u32 { + if let Some(rect) = self.single_pixel_rect(cursor_pos) { + let (texture, _depth) = self.attachments.unwrap(); - if single_pixel_rect.left < target_w && single_pixel_rect.bottom < target_h { texture .main_level() .first_layer() .into_image(None) .unwrap() - .raw_read::>, u32>(&single_pixel_rect)[0][0] + .raw_read::>, u32>(&rect)[0][0] } else { 0 } } + + fn single_pixel_rect(&self, (cursor_x, cursor_y): (u32, u32)) -> Option { + let (target_w, target_h) = self.attachments.desired_size().unwrap(); + let left = cursor_x; + let bottom = target_h.saturating_sub(cursor_y + 1); + if left < target_w && bottom < target_h { + Some(glium::Rect { + left, + bottom, + width: 1, + height: 1, + }) + } else { + None + } + } } diff --git a/ui/src/gridview/render/render2d.rs b/ui/src/gridview/render/render2d.rs index 9005217b..62131eee 100644 --- a/ui/src/gridview/render/render2d.rs +++ b/ui/src/gridview/render/render2d.rs @@ -20,31 +20,83 @@ use anyhow::{Context, Result}; use glium::glutin::event::ModifiersState; use glium::index::PrimitiveType; use glium::{uniform, Surface}; +use itertools::Itertools; +use palette::{Srgb, Srgba}; use ndcell_core::prelude::*; use Axis::{X, Y}; use super::consts::*; -use super::generic::{GenericGridViewRender, GridViewRenderDimension, LineParams}; +use super::generic::{GenericGridViewRender, GridViewRenderDimension, LineEndpoint2D, OverlayFill}; use super::shaders; use super::vertices::Vertex2D; use super::CellDrawParams; -use crate::config::MouseDragBinding; +use crate::commands::{DragCmd, DrawMode}; use crate::ext::*; use crate::gridview::*; -use crate::mouse::MouseDisplay; -use crate::{Scale, CONFIG}; +use crate::{Direction, Face, Scale, CONFIG, DIRECTIONS}; pub(in crate::gridview) type GridViewRender2D<'a> = GenericGridViewRender<'a, RenderDim2D>; -#[derive(Default)] pub(in crate::gridview) struct RenderDim2D; -impl GridViewRenderDimension<'_> for RenderDim2D { +impl<'a> GridViewRenderDimension<'a> for RenderDim2D { type D = Dim2D; type Viewpoint = Viewpoint2D; + type OverlayQuad = OverlayQuad; - const DEFAULT_COLOR: (f32, f32, f32, f32) = crate::colors::BACKGROUND_2D; + const DEFAULT_COLOR: Srgb = crate::colors::BACKGROUND_2D; const DEFAULT_DEPTH: f32 = 0.0; + const LINE_MIN_PIXEL_WIDTH: f64 = LINE_MIN_PIXEL_WIDTH_2D; + + fn init(_: &GridViewRender2D<'a>) -> Self { + Self + } + + fn draw_overlay_quads(this: &mut GridViewRender2D<'a>) -> Result<()> { + // Reborrow is necessary in order to split borrow. + let cache = &mut *this.cache; + let vbos = &mut cache.vbos; + let ibos = &mut cache.ibos; + + let gl_matrix = this.xform.gl_matrix_with_subpixel_offset(); + + let mut verts = vec![]; + for chunk in this.overlay_quads.chunks(QUAD_BATCH_SIZE) { + // Generate vertices. + verts.clear(); + for quad in chunk { + verts.extend_from_slice(&quad.verts()); + } + + // Populate VBO and IBO. + let quad_count = chunk.len(); + let vbo = vbos.quad_verts_2d(quad_count); + vbo.write(&verts); + let ibo = ibos.quad_indices(quad_count); + + // Draw quads. + this.params + .target + .draw( + vbo, + &ibo, + &shaders::RGBA_2D.load(), + &uniform! { matrix: gl_matrix }, + &glium::DrawParameters { + blend: glium::Blend::alpha_blending(), + depth: glium::Depth { + test: glium::DepthTest::Overwrite, + write: true, + ..Default::default() + }, + multisampling: false, + ..Default::default() + }, + ) + .context("Drawing 2D overlay quads")?; + } + Ok(()) + } } impl GridViewRender2D<'_> { @@ -63,7 +115,6 @@ impl GridViewRender2D<'_> { let gl_quadtree = cache.gl_quadtrees.gl_ndtree_from_node( (&visible_quadtree.root).into(), self.xform.render_cell_layer, - Self::ndtree_node_color, )?; // Step #2: draw at 1 pixel per render cell, including only the render // cells inside `self.local_visible_rect`. @@ -75,7 +126,7 @@ impl GridViewRender2D<'_> { .xform .global_to_local_int(&visible_quadtree.base_pos) .unwrap(); - let quadtree_offset = self.local_visible_rect.min() - local_quadtree_base; + let relative_quadtree_base = local_quadtree_base - self.local_visible_rect.min(); cells_fbo .draw( vbos.ndtree_quad(), @@ -86,7 +137,7 @@ impl GridViewRender2D<'_> { layer_count: gl_quadtree.layers, root_idx: gl_quadtree.root_idx, - quadtree_offset: quadtree_offset.to_i32_array(), + quadtree_base: relative_quadtree_base.to_i32_array(), }, &glium::DrawParameters { viewport: Some(cells_texture_viewport), @@ -114,6 +165,7 @@ impl GridViewRender2D<'_> { }, &glium::DrawParameters { blend: glium::Blend::alpha_blending(), + multisampling: false, ..Default::default() }, ) @@ -143,10 +195,8 @@ impl GridViewRender2D<'_> { Ok(()) } - /// Draws gridlines at varying opacity and spacing depending on scaling. - pub fn draw_gridlines(&mut self) -> Result<()> { - let mut gridline_overlay_rects = vec![]; - + /// Adds gridlines to the overlay. + pub fn add_gridlines_overlay(&mut self) { let min_gridline_exponent = self.gridline_cell_spacing_exponent(GRIDLINE_ALPHA_GRADIENT_LOW_PIXEL_SPACING); let max_gridline_exponent = @@ -154,517 +204,280 @@ impl GridViewRender2D<'_> { let range = (min_gridline_exponent..=max_gridline_exponent).rev(); for gridline_exponent in range { - gridline_overlay_rects.extend(self.generate_gridlines(gridline_exponent, X, Y)); - gridline_overlay_rects.extend(self.generate_gridlines(gridline_exponent, Y, X)); + self.add_gridline_set_overlay(gridline_exponent, X); + self.add_gridline_set_overlay(gridline_exponent, Y); } - - self.draw_cell_overlay_rects(&gridline_overlay_rects) - .context("Drawing gridlines") } - /// Generate gridlines overlay at one exponential power along one axis. - fn generate_gridlines( - &mut self, - gridline_exponent: u32, - parallel_axis: Axis, - perpendicular_axis: Axis, - ) -> impl Iterator { - let alpha = gridline_alpha(gridline_exponent, self.viewpoint.scale()); - let mut color = crate::colors::GRIDLINES; - color[3] *= alpha as f32; - let cell_spacing = BigInt::from(GRIDLINE_SPACING_COEFF) + /// Adds a set of gridlines at one exponential power along one axis. + fn add_gridline_set_overlay(&mut self, gridline_exponent: u32, perpendicular_axis: Axis) { + let alpha = gridline_alpha(gridline_exponent, self.viewpoint.scale()) as f32; + + let global_spacing: BigInt = BigInt::from(GRIDLINE_SPACING_COEFF) * BigInt::from(GRIDLINE_SPACING_BASE).pow(gridline_exponent); + let global_gridline_origin = self.xform.origin.div_floor(&global_spacing) * &global_spacing; + let local_gridline_origin = self + .xform + .global_to_local_float(&global_gridline_origin.to_fixedvec()) + .unwrap(); + let local_offset: R64 = local_gridline_origin[perpendicular_axis]; + let global_spacing: FixedPoint = global_spacing.into(); + let local_spacing: FixedPoint = global_spacing >> self.xform.render_cell_layer.to_u32(); + let local_spacing: R64 = r64(local_spacing.to_f64().unwrap()); // Compute the coordinate of the first gridline by rounding to the - // nearest multiple of `cell_spacing`. - let cell_start_coord = self.global_visible_rect.min()[perpendicular_axis] - .div_ceil(&cell_spacing) - * &cell_spacing; - let cell_end_coord = self.global_visible_rect.max()[perpendicular_axis].clone(); - - let cell_coords = itertools::iterate(cell_start_coord, move |coord| coord + &cell_spacing) - .take_while(move |coord| *coord <= cell_end_coord); + // nearest multiple of `local_spacing` (offset by `local_offset`). + let cell_start_coord: R64 = r64(self.local_visible_rect.min()[perpendicular_axis] as f64); + let cell_start_coord = ((cell_start_coord - local_offset) / local_spacing).ceil() + * local_spacing + + local_offset; - let mut start = self.local_visible_rect.min(); - let mut end = self.local_visible_rect.max() + 1; + let cell_end_coord = r64(self.local_visible_rect.max()[perpendicular_axis] as f64); - let render_cell_len = self.xform.render_cell_layer.big_len(); - let origin_coordinate = self.xform.origin[perpendicular_axis].clone(); + let cell_coords = itertools::iterate(cell_start_coord, |&coord| coord + local_spacing) + .take_while(|&coord| coord <= cell_end_coord); - let render_cell_coords = cell_coords.map(move |cell_coordinate| { - (FixedPoint::from(cell_coordinate - &origin_coordinate) / &render_cell_len) - .to_isize() - .unwrap() - }); + let visible_rect = self.local_visible_rect.to_frect(); - render_cell_coords.map(move |render_cell_coordinate| { + for coordinate in cell_coords { // Generate an individual line. - start[perpendicular_axis] = render_cell_coordinate; - end[perpendicular_axis] = render_cell_coordinate; - - CellOverlayRect { - start, - end, - z: GRIDLINE_DEPTH, - start_color: color, - end_color: color, - line_params: Some(LineParams { - width: GRIDLINE_WIDTH, - include_endpoints: true, - axis: parallel_axis, - }), - } - }) + let mut color = crate::colors::GRIDLINES; + color.alpha *= alpha; // (Alpha channel is linear according to OpenGL.) + + let mut a = visible_rect.min(); + let mut b = visible_rect.max(); + a[perpendicular_axis] = coordinate; + b[perpendicular_axis] = coordinate; + + self.add_line_overlay( + LineEndpoint2D::include(a, color), + LineEndpoint2D::include(b, color), + r64(GRIDLINE_WIDTH), + ); + } } - /// Draws a highlight on the render cell under the mouse cursor. - pub fn draw_hover_highlight(&mut self, cell_pos: &BigVec2D, color: [f32; 4]) -> Result<()> { - self.draw_cell_overlay_rects(&self.generate_cell_rect_outline( - IRect2D::single_cell(self.clamp_int_pos_to_visible(cell_pos)), - CURSOR_DEPTH, - HOVER_HIGHLIGHT_WIDTH, - color, - RectHighlightParams { - fill: true, - crosshairs: true, - }, - )) - .context("Drawing cursor highlight") + /// Adds a highlight on the render cell under the mouse cursor when using + /// the drawing tool. + pub fn add_hover_draw_overlay(&mut self, cell_pos: &BigVec2D, draw_mode: DrawMode) { + self.add_hover_overlay(cell_pos, draw_mode.fill_color(), draw_mode.outline_color()); + } + /// Adds a highlight on the render cell under the mouse cursor when using + /// the selection tool. + pub fn add_hover_select_overlay(&mut self, cell_pos: &BigVec2D) { + use crate::colors::hover::*; + self.add_hover_overlay(cell_pos, SELECT_FILL, SELECT_OUTLINE); + } + /// Adds a highlight on the render cell under the mouse cursor. + fn add_hover_overlay(&mut self, cell_pos: &BigVec2D, fill_color: Srgba, outline_color: Srgb) { + let local_rect = IRect::single_cell(self.clamp_int_pos_to_visible(cell_pos)); + let width = r64(HOVER_HIGHLIGHT_WIDTH); + self.add_rect_fill_overlay(local_rect, fill_color); + self.add_rect_crosshairs_overlay(local_rect, outline_color, width); + } + + /// Adds a highlight around the selection when the selection includes cells. + pub fn add_selection_cells_highlight_overlay(&mut self, selection_rect: &BigRect2D) { + use crate::colors::selection::*; + self.add_selection_highlight_overlay(selection_rect, CELLS_FILL, CELLS_OUTLINE) + } + /// Adds a highlight around the selection when the selection does not + /// include cells. + pub fn add_selection_region_highlight_overlay(&mut self, selection_rect: &BigRect2D) { + use crate::colors::selection::*; + self.add_selection_highlight_overlay(selection_rect, REGION_FILL, REGION_OUTLINE) } - /// Draws a highlight around the selected rectangle. - pub fn draw_selection_highlight( + /// Adds a highlight around the selection. + fn add_selection_highlight_overlay( &mut self, - selection_rect: BigRect2D, - fill: bool, - ) -> Result<()> { - let visible_selection_rect = self.clip_int_rect_to_visible(&selection_rect); - - self.draw_cell_overlay_rects(&self.generate_cell_rect_outline( - visible_selection_rect, - SELECTION_DEPTH, - SELECTION_HIGHLIGHT_WIDTH, - crate::colors::SELECTION, - RectHighlightParams { - fill, - crosshairs: false, - }, - )) - .context("Drawing selection highlight")?; + selection_rect: &BigRect2D, + fill_color: Srgba, + outline_color: Srgb, + ) { + let local_rect = self.clip_int_rect_to_visible(selection_rect); + let width = r64(SELECTION_HIGHLIGHT_WIDTH); + self.add_rect_fill_overlay(local_rect, fill_color); + self.add_rect_outline_overlay(local_rect, outline_color, width); + + let local_rect = local_rect.to_frect(); // "Move selected cells" target. self.add_mouse_target_quad( - ModifiersState::empty(), - visible_selection_rect.to_frect(), + local_rect, + Some(ModifiersState::empty()), MouseTargetData { - binding: Some(MouseDragBinding::Select( - SelectDragCommand::MoveCells.into(), - )), - display: MouseDisplay::Move, + binding: DragCmd::MoveSelectedCells(None), }, ); + // "Move selection" target. self.add_mouse_target_quad( - ModifiersState::SHIFT, - visible_selection_rect.to_frect(), + local_rect, + Some(ModifiersState::SHIFT), MouseTargetData { - binding: Some(MouseDragBinding::Select( - SelectDragCommand::MoveSelection.into(), - )), - display: MouseDisplay::Move, + binding: DragCmd::MoveSelection(None), }, ); + // "Move copy of cells" target. self.add_mouse_target_quad( - ModifiersState::CTRL, - visible_selection_rect.to_frect(), + local_rect, + Some(ModifiersState::CTRL), MouseTargetData { - binding: Some(MouseDragBinding::Select( - SelectDragCommand::CopyCells.into(), - )), - display: MouseDisplay::Move, + binding: DragCmd::CopySelectedCells(None), }, ); // "Resize selection" target. - let click_target_width = CONFIG.lock().ctrl.selection_resize_drag_target_width + let click_target_width = r64(CONFIG.lock().ctrl.selection_resize_drag_target_width) * self.xform.render_cell_scale.inv_factor().to_f64().unwrap(); - let (min, max) = ( - visible_selection_rect.min(), - visible_selection_rect.max() + 1, - ); + let (min, max) = (local_rect.min(), local_rect.max()); let xs = vec![ - r64(min[X] as f64 - click_target_width * 0.75), - r64(min[X] as f64 + click_target_width * 0.25), - r64(max[X] as f64 - click_target_width * 0.25), - r64(max[X] as f64 + click_target_width * 0.75), + min[X] - click_target_width * 0.75, + min[X] + click_target_width * 0.25, + max[X] - click_target_width * 0.25, + max[X] + click_target_width * 0.75, ]; let ys = vec![ - r64(min[Y] as f64 - click_target_width * 0.75), - r64(min[Y] as f64 + click_target_width * 0.25), - r64(max[Y] as f64 - click_target_width * 0.25), - r64(max[Y] as f64 + click_target_width * 0.75), - ]; - let x_indices = vec![0, 1, 2, 0, 2, 0, 1, 2]; - let y_indices = vec![0, 0, 0, 1, 1, 2, 2, 2]; - let mouse_displays = vec![ - MouseDisplay::ResizeNESW, - MouseDisplay::ResizeNS, - MouseDisplay::ResizeNWSE, - MouseDisplay::ResizeEW, - MouseDisplay::ResizeEW, - MouseDisplay::ResizeNWSE, - MouseDisplay::ResizeNS, - MouseDisplay::ResizeNESW, + min[Y] - click_target_width * 0.75, + min[Y] + click_target_width * 0.25, + max[Y] - click_target_width * 0.25, + max[Y] + click_target_width * 0.75, ]; - for ((xi, yi), display) in x_indices.into_iter().zip(y_indices).zip(mouse_displays) { - let mut axes = AxisSet::empty(); - if xi != 1 { - axes.add(X); - } - if yi != 1 { - axes.add(Y); - } - let binding = Some(MouseDragBinding::Select( - SelectDragCommand::Resize { axes, plane: None }.into(), - )); + for &direction in &DIRECTIONS { + let binding = DragCmd::ResizeSelection2D(direction); + let NdVec([dx, dy]) = direction.vector(); self.add_mouse_target_quad( - ModifiersState::empty(), - FRect::span(NdVec([xs[xi], ys[yi]]), NdVec([xs[xi + 1], ys[yi + 1]])), - MouseTargetData { binding, display }, + FRect::span( + NdVec([xs[(dx + 1) as usize], ys[(dy + 1) as usize]]), + NdVec([xs[(dx + 2) as usize], ys[(dy + 2) as usize]]), + ), + Some(ModifiersState::empty()), + MouseTargetData { binding }, ); } - - Ok(()) } - /// Draws a highlight indicating how the selection will be resized. - pub fn draw_absolute_selection_resize_preview( + /// Adds a highlight indicating how the selection will be resized. + pub fn add_selection_resize_preview_overlay(&mut self, selection_preview_rect: &BigRect2D) { + let local_rect = self.clip_int_rect_to_visible(&selection_preview_rect); + let width = r64(SELECTION_HIGHLIGHT_WIDTH); + use crate::colors::selection::*; + self.add_rect_fill_overlay(local_rect, RESIZE_FILL); + self.add_rect_outline_overlay(local_rect, RESIZE_OUTLINE, width); + } + /// Adds a highlight indicating which edge(s) of the selection will be resized. + pub fn add_selection_edge_resize_overlay( &mut self, - selection_rect: BigRect2D, - mouse_pos: &ScreenPos2D, - ) -> Result<()> { - let selection_preview_rect = selection::resize_selection_absolute( - &selection_rect, - &mouse_pos.pos(), - &mouse_pos.rect(), - ); - let visible_selection_preview_rect = self.clip_int_rect_to_visible(&selection_preview_rect); - self.draw_cell_overlay_rects(&self.generate_cell_rect_outline( - visible_selection_preview_rect, - SELECTION_RESIZE_DEPTH, - SELECTION_RESIZE_PREVIEW_WIDTH, - crate::colors::SELECTION_RESIZE, - RectHighlightParams { - fill: true, - crosshairs: false, - }, - )) - .context("Drawing selection resize highlight")?; - Ok(()) + selection_rect: &BigRect2D, + direction: Direction, + ) { + let color = crate::colors::selection::RESIZE_OUTLINE; + let rect = self._adjust_rect_for_overlay(self.clip_int_rect_to_visible(selection_rect)); + for &ax in Dim2D::axes() { + let mut min = rect.min(); + let mut max = rect.max(); + match direction.vector()[ax] { + -1 => max[ax] = min[ax], + 1 => min[ax] = max[ax], + _ => continue, + }; + self.add_line_overlay( + LineEndpoint2D::include(min, color), + LineEndpoint2D::include(max, color), + r64(SELECTION_HIGHLIGHT_WIDTH), + ); + } } - /// Generates a cell overlay to outline the given cell rectangle, with - /// optional fill and crosshairs. - #[must_use = "This method only generates the rectangles; call `draw_cell_overlay_rects` to draw them"] - fn generate_cell_rect_outline( - &self, - rect: IRect2D, - z: f32, - width: f64, - color: [f32; 4], - params: RectHighlightParams, - ) -> Vec { - let bright_color = color; - let mut dull_color = color; - dull_color[3] *= 0.25; - let mut fill_color = color; - fill_color[0] *= 0.5; - fill_color[1] *= 0.5; - fill_color[2] *= 0.5; - fill_color[3] *= 0.75; - - let NdVec([min_x, min_y]) = self.local_visible_rect.min(); - let NdVec([max_x, max_y]) = self.local_visible_rect.max() + 1; - - // If there are more than 1.5 pixels per render cell, the upper boundary - // should be *between* cells (+1). If there are fewer, the upper - // boundary should be *on* the cell (+0). - let pixels_per_cell = self.xform.render_cell_scale.units_per_cell(); - let a = rect.min(); - let b = rect.max() + (pixels_per_cell > 1.5) as isize; - let NdVec([ax, ay]) = a; - let NdVec([bx, by]) = b; - - let mut h_stops = vec![ - (min_x, dull_color), - (ax - 1, dull_color), - (ax, bright_color), - (bx, bright_color), - (bx + 1, dull_color), - (max_x, dull_color), - ]; - let mut v_stops = vec![ - (min_y, dull_color), - (ay - 1, dull_color), - (ay, bright_color), - (by, bright_color), - (by + 1, dull_color), - (max_y, dull_color), + /// Adds a filled-in rectangle with a solid color. + fn add_rect_fill_overlay(&mut self, rect: IRect2D, color: Srgba) { + self.overlay_quads.push(OverlayQuad { + rect: self._adjust_rect_for_overlay(rect), + fill: OverlayFill::Solid(color), + }); + } + /// Adds a rectangular outline with a solid color. + fn add_rect_outline_overlay(&mut self, rect: IRect2D, color: Srgb, width: R64) { + let rect = self._adjust_rect_for_overlay(rect); + let NdVec([ax, ay]) = rect.min(); + let NdVec([bx, by]) = rect.max(); + + let corners = [ + NdVec([ax, ay]), + NdVec([bx, ay]), + NdVec([bx, by]), + NdVec([ax, by]), ]; - // Optionally remove crosshairs. - if !params.crosshairs { - h_stops = h_stops - .into_iter() - .filter(|&(_, color)| color == bright_color) - .collect(); - v_stops = v_stops - .into_iter() - .filter(|&(_, color)| color == bright_color) - .collect(); - } - - let mut ret = vec![]; - - // In case the crosshairs/outline is transparent, render gridlines - // beneath it. Draw order works in our favor here: - // https://stackoverflow.com/a/20231235/4958484 - if params.crosshairs { - ret.extend_from_slice(&self.generate_solid_cell_borders( - vec![ax, bx], - vec![ay, by], - z - TINY_OFFSET, - width, - crate::colors::GRIDLINES, - )); - } - - // Generate lines. - for &x in &[ax, bx] { - ret.extend_from_slice(&self.generate_gradient_cell_border( - v_stops.iter().map(|&(y, color)| (NdVec([x, y]), color)), - z, + for (&corner1, &corner2) in corners.iter().circular_tuple_windows() { + self.add_line_overlay( + LineEndpoint2D::include(corner1, color), + LineEndpoint2D::include(corner2, color), width, - Y, - )); - } - for &y in &[ay, by] { - ret.extend_from_slice(&self.generate_gradient_cell_border( - h_stops.iter().map(|&(x, color)| (NdVec([x, y]), color)), - z, - width, - X, - )); - } - - // Generate fill after lines. - if params.fill { - ret.push(CellOverlayRect { - start: a, - end: b, - z, - start_color: fill_color, - end_color: fill_color, - line_params: None, - }) + ); } - - ret } - - /// Generates a cell overlay for solid borders along the given columns and - /// rows. - #[must_use = "This method only generates the rectangles; call `draw_cell_overlay_rects` to draw them"] - fn generate_solid_cell_borders( - &self, - columns: impl IntoIterator, - rows: impl IntoIterator, - z: f32, - width: f64, - color: [f32; 4], - ) -> Vec { - let min = self.local_visible_rect.min(); - let max = self.local_visible_rect.max() + 1; - let min_x = min[X]; - let min_y = min[Y]; - let max_x = max[X]; - let max_y = max[Y]; - - let h_line_params = Some(LineParams { - width, - include_endpoints: true, - axis: X, - }); - let v_line_params = Some(LineParams { - width, - include_endpoints: true, - axis: Y, - }); - - let mut ret = Vec::with_capacity(4 * self.local_visible_rect.size().sum() as usize); - for x in columns { - ret.push(CellOverlayRect { - start: NdVec([x, min_y]), - end: NdVec([x, max_y]), - z, - start_color: color, - end_color: color, - line_params: v_line_params, - }); - } - for y in rows { - ret.push(CellOverlayRect { - start: NdVec([min_x, y]), - end: NdVec([max_x, y]), - z, - start_color: color, - end_color: color, - line_params: h_line_params, - }); - } - ret + /// Adds crosshairs around a rectangle with a solid color that fades into + /// the gridline color toward the edges. + fn add_rect_crosshairs_overlay(&mut self, rect: IRect2D, color: Srgb, width: R64) { + let rect = self._adjust_rect_for_overlay(rect); + let NdVec([ax, ay]) = rect.min(); + let NdVec([bx, by]) = rect.max(); + + self.add_single_crosshair_overlay(X, ax, bx, ay, width, color); // bottom + self.add_single_crosshair_overlay(X, ax, bx, by, width, color); // top + self.add_single_crosshair_overlay(Y, ay, by, ax, width, color); // left + self.add_single_crosshair_overlay(Y, ay, by, bx, width, color); // right } - - /// Generates a cell overlay for a gradient cell border. - #[must_use = "This method only generates the rectangles; call `draw_cell_overlay_rects` to draw them"] - fn generate_gradient_cell_border( - &self, - stops: impl IntoIterator, - z: f32, - width: f64, - axis: Axis, - ) -> Vec { - // Generate a rectangle for each stop (so that there is a definitive - // color at each point) AND a rectangle between each adjacent pair of - // stops. - let mut ret = vec![]; - let mut prev_stop = None; - let btwn_stops_line_params = Some(LineParams { - width, - include_endpoints: false, - axis, - }); - let single_stop_line_params = Some(LineParams { - width, - include_endpoints: true, - axis, - }); - for stop in stops { - let (pos, color) = stop; - if let Some((prev_pos, prev_color)) = prev_stop { - ret.push(CellOverlayRect { - start: prev_pos, - end: pos, - z, - start_color: prev_color, - end_color: color, - line_params: btwn_stops_line_params, - }); - } - ret.push(CellOverlayRect { - start: pos, - end: pos, - z, - start_color: color, - end_color: color, - line_params: single_stop_line_params, - }); - prev_stop = Some(stop); + fn add_single_crosshair_overlay( + &mut self, + parallel_axis: Axis, + a: R64, + b: R64, + perpendicular_coord: R64, + width: R64, + color: Srgb, + ) { + let mut pos = NdVec::repeat(perpendicular_coord); + pos[parallel_axis] = a; + let endpoint_pairs = self.make_crosshair_endpoints(parallel_axis, a, b, pos, color); + for [start, end] in endpoint_pairs { + self.add_line_overlay(start, end, width); } - ret } - - /// Draws a cell overlay. - fn draw_cell_overlay_rects(&mut self, rects: &[CellOverlayRect]) -> Result<()> { - let mut verts = vec![]; - - // Draw the rectangles in batches, because the VBO might not be able to - // hold all the vertices at once. - for rect_batch in rects.chunks(QUAD_BATCH_SIZE) { - let count = rect_batch.len(); - // Generate vertices. - verts.clear(); - for &rect in rect_batch { - verts.extend_from_slice(&self.make_cell_overlay_verts(rect)); - } - - // Reborrow is necessary in order to split borrow. - let cache = &mut *self.cache; - let ibos = &mut cache.ibos; - let vbos = &mut cache.vbos; - - // Put the data in a slice of the VBO. - let vbo_slice = vbos.quad_verts_2d(count); - vbo_slice.write(&verts); - // Draw rectangles. - self.params - .target - .draw( - vbo_slice, - &ibos.quad_indices(count), - &shaders::RGBA_2D.load(), - &uniform! { matrix: self.xform.gl_matrix_with_subpixel_offset() }, - &glium::DrawParameters { - blend: glium::Blend::alpha_blending(), - depth: glium::Depth { - test: glium::DepthTest::IfMore, - write: true, - ..Default::default() - }, - ..Default::default() - }, - ) - .context("Drawing cell-aligned rectangles")?; - } - Ok(()) + fn add_line_overlay(&mut self, mut start: LineEndpoint2D, mut end: LineEndpoint2D, width: R64) { + let (rect, axis) = self.make_line_ndrect(&mut start, &mut end, width); + let fill = OverlayFill::gradient(axis, start.color, end.color); + self.overlay_quads.push(OverlayQuad { rect, fill }) } - fn make_cell_overlay_verts(&self, rect: CellOverlayRect) -> [Vertex2D; 4] { - let mut a = rect.start.to_fvec(); - let mut b = rect.end.to_fvec(); - let mut colors = [ - rect.start_color, - rect.start_color, - rect.end_color, - rect.end_color, + fn add_mouse_target_quad( + &mut self, + rect: FRect2D, + modifiers: Option, + data: MouseTargetData, + ) { + let NdVec([x1, y1]) = rect.min(); + let NdVec([x2, y2]) = rect.max(); + let z = r64(0.0); + let corners = [ + NdVec([x1, y1, z]), + NdVec([x2, y1, z]), + NdVec([x1, y2, z]), + NdVec([x2, y2, z]), ]; - if let Some(LineParams { - width, - include_endpoints, - axis, - }) = rect.line_params - { - // At this point, the rectangle should have zero extra width. - let min_width = self.xform.render_cell_scale.cells_per_unit(); // 1 pixel - let width = if self.xform.render_cell_layer == Layer(0) { - std::cmp::max(r64(width), min_width) - } else { - min_width - }; - let offset = FVec::repeat(width / 2.0) * (b - a).signum(); - // Expand it in all directions, so now it has the correct width and - // includes its endpoints. - a -= offset; - b += offset; - // Now exclude the endpoints, if requested. - if !include_endpoints { - a[axis] += offset[axis] * 2.0; - b[axis] -= offset[axis] * 2.0; - } - if axis == X { - // Use horizontal gradient instead of vertical gradient. - colors.swap(1, 2); - } - } - let ax = a[X].to_f32().unwrap(); - let ay = a[Y].to_f32().unwrap(); - let bx = b[X].to_f32().unwrap(); - let by = b[Y].to_f32().unwrap(); - [ - Vertex2D::from(([ax, ay, rect.z], colors[0])), - Vertex2D::from(([bx, ay, rect.z], colors[1])), - Vertex2D::from(([ax, by, rect.z], colors[2])), - Vertex2D::from(([bx, by, rect.z], colors[3])), - ] + let target_id = self.add_mouse_target(data); + self.add_mouse_target_tri(modifiers, [corners[0], corners[1], corners[2]], target_id); + self.add_mouse_target_tri(modifiers, [corners[3], corners[2], corners[1]], target_id); + } + + fn _adjust_rect_for_overlay(&self, rect: IRect2D) -> FRect2D { + // If there are more than 1.5 pixels per render cell, the upper outline + // should be *between* cells (+0.0). If there are fewer, the upper + // outline should be *on* the cell below (-1.0). + let pix_per_cell = self.xform.render_cell_scale.units_per_cell(); + let upper_offset = if pix_per_cell > 1.5 { 0.0 } else { -1.0 }; + rect.to_frect().offset_min_max(r64(0.0), r64(upper_offset)) } } @@ -710,35 +523,22 @@ fn clamped_interpolate(x: f64, min: f64, max: f64, min_result: f64, max_result: /// Because `glLineWidth` is not supported on all platforms, we draw rectangles /// to vary gridline width. #[derive(Debug, Copy, Clone)] -struct CellOverlayRect { - /// Start point of a line, or one corner of a rectangle. - start: IVec2D, - /// End point of a line, or other corner of a rectangle. - end: IVec2D, - /// Z order. - z: f32, - /// Color at the start of the line. - start_color: [f32; 4], - /// Color at the end of the line. - end_color: [f32; 4], - /// Optional parameters for lines. - line_params: Option, +pub struct OverlayQuad { + /// 2D region. + rect: FRect2D, + /// Color fill. + fill: OverlayFill, } -impl CellOverlayRect { - fn solid_rect(rect: IRect2D, z: f32, color: [f32; 4]) -> Self { - Self { - start: rect.min(), - end: rect.max() + 1, - z, - start_color: color, - end_color: color, - line_params: None, - } +impl OverlayQuad { + fn verts(self) -> [Vertex2D; 4] { + let [x1, y1] = self.rect.min().to_f32_array(); + let [x2, y2] = self.rect.max().to_f32_array(); + let colors = self.fill.vertex_colors(Face::PosZ); + [ + Vertex2D::new([x1, y1], colors[0]), + Vertex2D::new([x2, y1], colors[1]), + Vertex2D::new([x1, y2], colors[2]), + Vertex2D::new([x2, y2], colors[3]), + ] } } - -#[derive(Debug, Copy, Clone)] -struct RectHighlightParams { - pub fill: bool, - pub crosshairs: bool, -} diff --git a/ui/src/gridview/render/render3d.rs b/ui/src/gridview/render/render3d.rs deleted file mode 100644 index 505d9fc0..00000000 --- a/ui/src/gridview/render/render3d.rs +++ /dev/null @@ -1,363 +0,0 @@ -//! 3D grid rendering. -//! -//! Currently, only solid colors are supported, however I plan to add custom -//! models and maybe textures in the future. - -use anyhow::{Context, Result}; -use glium::index::PrimitiveType; -use glium::Surface; - -use ndcell_core::prelude::*; -use Axis::{X, Y, Z}; - -use super::consts::*; -use super::generic::{GenericGridViewRender, GridViewRenderDimension}; -use super::shaders; -use super::vertices::Vertex3D; -use super::CellDrawParams; -use crate::ext::*; -use crate::gridview::*; -use crate::CONFIG; - -pub(in crate::gridview) type GridViewRender3D<'a> = GenericGridViewRender<'a, RenderDim3D>; - -type QuadVerts = [Vertex3D; 4]; -type CuboidVerts = [Option; 6]; - -#[derive(Default)] -pub(in crate::gridview) struct RenderDim3D { - fog_center: [f32; 3], - fog_start: f32, - fog_end: f32, -} -impl<'a> GridViewRenderDimension<'a> for RenderDim3D { - type D = Dim3D; - type Viewpoint = Viewpoint3D; - - const DEFAULT_COLOR: (f32, f32, f32, f32) = crate::colors::BACKGROUND_3D; - const DEFAULT_DEPTH: f32 = f32::INFINITY; - - fn init(mut this: GridViewRender3D<'a>) -> GridViewRender3D<'a> { - this.dim.fog_center = this - .xform - .global_to_local_float(this.viewpoint.center()) - .unwrap() - .to_f32_array(); - - let inv_scale_factor = this.xform.render_cell_scale.inv_factor().to_f32().unwrap(); - this.dim.fog_end = Viewpoint3D::VIEW_RADIUS * inv_scale_factor; - - this.dim.fog_start = FOG_START_FACTOR * this.dim.fog_end; - - this - } -} - -impl GridViewRender3D<'_> { - /// Draw an ND-tree to scale on the target. - pub fn draw_cells(&mut self, params: CellDrawParams<'_, Dim3D>) -> Result<()> { - let visible_octree = match self.clip_ndtree_to_visible(¶ms) { - Some(x) => x, - None => return Ok(()), // There is nothing to draw. - }; - - let octree_offset = self - .xform - .global_to_local_int(&visible_octree.base_pos) - .unwrap(); - - // Reborrow is necessary in order to split borrow. - let cache = &mut *self.cache; - let vbos = &mut cache.vbos; - - let gl_octree = cache.gl_octrees.gl_ndtree_from_node( - (&visible_octree.root).into(), - self.xform.render_cell_layer, - Self::ndtree_node_color, - )?; - - self.params - .target - .draw( - &*vbos.ndtree_quad(), - &glium::index::NoIndices(PrimitiveType::TriangleStrip), - &shaders::OCTREE.load(), - &uniform! { - matrix: self.xform.gl_matrix(), - - octree_texture: &gl_octree.texture, - layer_count: gl_octree.layers, - root_idx: gl_octree.root_idx, - - octree_offset: octree_offset.to_i32_array(), - - perf_view: CONFIG.lock().gfx.octree_perf_view, - - light_direction: LIGHT_DIRECTION, - light_ambientness: LIGHT_AMBIENTNESS, - max_light: MAX_LIGHT, - - fog_color: crate::colors::BACKGROUND_3D, - fog_center: self.dim.fog_center, - fog_start: self.dim.fog_start, - fog_end: self.dim.fog_end, - }, - &glium::DrawParameters { - depth: glium::Depth { - test: glium::DepthTest::IfLessOrEqual, - write: true, - ..glium::Depth::default() - }, - blend: glium::Blend::alpha_blending(), - smooth: Some(glium::Smooth::Nicest), - ..Default::default() - }, - ) - .context("Drawing cells")?; - - Ok(()) - } - - pub fn draw_gridlines(&mut self) -> Result<()> { - let (grid_x, grid_y) = (X, Y); - let perpendicular_axis = Z; - let perpendicular_coordinate = BigInt::zero(); - - let mut min = self.local_visible_rect.min().to_fvec(); - let mut max = self.local_visible_rect.max().to_fvec(); - min[Z] = r64(0.0); - max[Z] = r64(0.0); - let quad = FRect2D::span( - NdVec([min[grid_x], min[grid_y]]), - NdVec([max[grid_x], max[grid_y]]), - ); - - // Compute the coefficient for the smallest visible gridlines. - let log2_cell_spacing = (GRIDLINE_SPACING_BASE as f32).log2() - * (self.gridline_cell_spacing_exponent(1.0) as f32) - + (GRIDLINE_SPACING_COEFF as f32).log2(); - let log2_render_cell_spacing = - log2_cell_spacing - (self.xform.render_cell_layer.to_u32() as f32); - let coefficient = log2_render_cell_spacing.exp2(); - - let mut global_grid_origin: BigVec3D; - let mut max_exponents: IVec3D; - { - // Compute the largest gridline spacing that fits within the visible - // area. - let max_visible_exponent = - self.gridline_cell_spacing_exponent(Viewpoint3D::VIEW_RADIUS as f64 * 2.0); - let max_visible_spacing = BigInt::from(GRIDLINE_SPACING_BASE) - .pow(max_visible_exponent + 1) - * GRIDLINE_SPACING_COEFF; - // Round to nearest multiple of that spacing. - global_grid_origin = - self.xform.origin.div_floor(&max_visible_spacing) * &max_visible_spacing; - - // Compute the maximum exponent that will be visible for each axis. - // There is a similar loop in the 3D gridlines fragment shader. - let spacing_coefficient: BigInt = GRIDLINE_SPACING_COEFF.into(); - let spacing_base: BigInt = GRIDLINE_SPACING_BASE.into(); - let mut tmp = global_grid_origin.div_floor(&spacing_coefficient); - max_exponents = IVec3D::repeat(0); - for &ax in &[grid_x, grid_y] { - const LARGE_EXPONENT: isize = 100; - if tmp[ax].is_zero() { - max_exponents[ax] = LARGE_EXPONENT; - } else { - while tmp[ax].mod_floor(&spacing_base).is_zero() - && max_exponents[ax] < LARGE_EXPONENT - { - tmp[ax] /= GRIDLINE_SPACING_BASE; - max_exponents[ax] += 1; - } - } - } - } - global_grid_origin[perpendicular_axis] = perpendicular_coordinate; - let max_exponents: IVec2D = NdVec([max_exponents[grid_x], max_exponents[grid_y]]); - - let local_grid_origin = self - .xform - .global_to_local_float(&global_grid_origin.to_fixedvec()) - .unwrap(); - - // Reborrow is necessary in order to split borrow. - let cache = &mut *self.cache; - let vbos = &mut cache.vbos; - let ibos = &mut cache.ibos; - - self.params - .target - .draw( - &*vbos.gridlines_quad(quad), - &ibos.quad_indices(1), - &shaders::GRIDLINES_3D.load(), - &uniform! { - matrix: self.xform.gl_matrix(), - - grid_axes: [grid_x as i32, grid_y as i32], - grid_color: crate::colors::GRIDLINES, - grid_origin: local_grid_origin.to_f32_array(), - grid_coefficient: coefficient, - grid_base: GRIDLINE_SPACING_BASE as i32, - grid_max_exponents: max_exponents.to_i32_array(), - min_line_spacing: GRIDLINE_ALPHA_GRADIENT_LOW_PIXEL_SPACING as f32, - max_line_spacing: GRIDLINE_ALPHA_GRADIENT_HIGH_PIXEL_SPACING as f32, - line_width: if self.xform.render_cell_layer == Layer(0) { - GRIDLINE_WIDTH as f32 - } else { - 0.0 // minimum width of one pixel - }, - - fog_color: crate::colors::BACKGROUND_3D, - fog_center: self.dim.fog_center, - fog_start: self.dim.fog_start, - fog_end: self.dim.fog_end, - }, - &glium::DrawParameters { - depth: glium::Depth { - test: glium::DepthTest::IfLessOrEqual, - write: true, - ..glium::Depth::default() - }, - blend: glium::Blend::alpha_blending(), - backface_culling: glium::BackfaceCullingMode::CullingDisabled, - ..Default::default() - }, - ) - .context("Drawing gridlines")?; - - Ok(()) - } - - fn draw_quads(&mut self, quad_verts: &[Vertex3D]) -> Result<()> { - // Reborrow is necessary in order to split borrow. - let cache = &mut *self.cache; - let vbos = &mut cache.vbos; - let ibos = &mut cache.ibos; - - for chunk in quad_verts.chunks(4 * QUAD_BATCH_SIZE) { - let count = chunk.len() / 4; - - // Copy that into a VBO. - let vbo_slice = vbos.quad_verts_3d(count); - vbo_slice.write(&chunk); - - self.params - .target - .draw( - vbo_slice, - &ibos.quad_indices(count), - &shaders::GRIDLINES_3D.load(), - &uniform! { - matrix: self.xform.gl_matrix(), - - light_direction: LIGHT_DIRECTION, - light_ambientness: LIGHT_AMBIENTNESS, - max_light: MAX_LIGHT, - - fog_color: crate::colors::BACKGROUND_3D, - fog_center: self.dim.fog_center, - fog_start: self.dim.fog_start, - fog_end: self.dim.fog_end, - }, - &glium::DrawParameters { - depth: glium::Depth { - test: glium::DepthTest::IfLessOrEqual, - write: true, - ..glium::Depth::default() - }, - blend: glium::Blend::alpha_blending(), - smooth: Some(glium::Smooth::Nicest), - ..Default::default() - }, - ) - .context("Drawing faces to target")?; - } - - Ok(()) - } -} - -/* -fn cuboid_verts(real_camera_pos: FVec3D, cuboid: FRect3D, color: [u8; 3]) -> CuboidVerts { - let make_face_verts = |axis, sign| face_verts(real_camera_pos, cuboid, (axis, sign), color); - [ - make_face_verts(X, Sign::Minus), - make_face_verts(X, Sign::Plus), - make_face_verts(Y, Sign::Minus), - make_face_verts(Y, Sign::Plus), - make_face_verts(Z, Sign::Minus), - make_face_verts(Z, Sign::Plus), - ] -} -fn face_verts( - real_camera_pos: FVec3D, - cuboid: FRect3D, - face: (Axis, Sign), - color: [u8; 3], -) -> Option { - let (face_axis, face_sign) = face; - - let normal = match face { - (X, Sign::Minus) => [i8::MIN, 0, 0], - (X, Sign::Plus) => [i8::MAX, 0, 0], - (Y, Sign::Minus) => [0, i8::MIN, 0], - (Y, Sign::Plus) => [0, i8::MAX, 0], - (Z, Sign::Minus) => [0, 0, i8::MIN], - (Z, Sign::Plus) => [0, 0, i8::MAX], - _ => return None, - }; - - let (mut ax1, mut ax2) = match face_axis { - X => (Y, Z), - Y => (Z, X), - Z => (X, Y), - _ => return None, - }; - if face_sign == Sign::Plus { - std::mem::swap(&mut ax1, &mut ax2); - } - - let mut pos0 = cuboid.min(); - let mut pos3 = cuboid.max(); - - // Backface culling - if real_camera_pos[face_axis] < pos3[face_axis] && face_sign == Sign::Plus { - // The camera is on the negative side, but this is the positive face. - return None; - } - if real_camera_pos[face_axis] > pos0[face_axis] && face_sign == Sign::Minus { - // The camera is on the positive side, but this is the negative face. - return None; - } - - match face_sign { - Sign::Minus => pos3[face_axis] = pos0[face_axis], - Sign::Plus => pos0[face_axis] = pos3[face_axis], - _ => return None, - } - - let mut pos1 = pos0; - pos1[ax1] = pos3[ax1]; - - let mut pos2 = pos0; - pos2[ax2] = pos3[ax2]; - - let [r, g, b] = color; - let color = [r, g, b, u8::MAX]; - - let pos_to_vertex = |NdVec([x, y, z]): FVec3D| Vertex3D { - pos: [x.raw() as f32, y.raw() as f32, z.raw() as f32], - normal, - color, - }; - Some([ - pos_to_vertex(pos0), - pos_to_vertex(pos1), - pos_to_vertex(pos2), - pos_to_vertex(pos3), - ]) -} -*/ diff --git a/ui/src/gridview/render/render3d/fog.rs b/ui/src/gridview/render/render3d/fog.rs new file mode 100644 index 00000000..3edc59f4 --- /dev/null +++ b/ui/src/gridview/render/render3d/fog.rs @@ -0,0 +1,38 @@ +use super::*; + +/// Fog rendering parameters. +/// +/// Fields are arranged to eliminate padding. +#[derive(Debug, Copy, Clone)] +pub(super) struct FogParams { + fog_color: [f32; 3], + fog_start: f32, + fog_center: [f32; 3], + fog_end: f32, +} + +implement_uniform_block!(FogParams, fog_color, fog_center, fog_start, fog_end); + +impl From<&GridViewRender3D<'_>> for FogParams { + fn from(gvr3d: &GridViewRender3D<'_>) -> Self { + let fog_color = crate::colors::BACKGROUND_3D.into_raw(); + + let fog_center = gvr3d + .xform + .global_to_local_float(gvr3d.viewpoint.center()) + .unwrap() + .to_f32_array(); + + let inv_scale_factor = gvr3d.xform.render_cell_scale.inv_factor().to_f32().unwrap(); + let fog_end = Viewpoint3D::VIEW_RADIUS * inv_scale_factor; + + let fog_start = FOG_START_FACTOR * fog_end; + + Self { + fog_color, + fog_center, + fog_start, + fog_end, + } + } +} diff --git a/ui/src/gridview/render/render3d/gridlines.rs b/ui/src/gridview/render/render3d/gridlines.rs new file mode 100644 index 00000000..038cc9f2 --- /dev/null +++ b/ui/src/gridview/render/render3d/gridlines.rs @@ -0,0 +1,116 @@ +use super::*; + +/// Gridline rendering parameters. +/// +/// Fields are arranged to eliminate padding. +#[derive(Debug, Default, Copy, Clone)] +pub(super) struct GridlineParams { + /// Color of gridlines. + grid_color: [f32; 4], + + /// Local position of the visible gridline with the largest exponent. + grid_origin: [f32; 3], + + /// Pixel width of gridlines. + grid_width: f32, + + /// Gridline exponent along each axis at `origin`. + grid_max_exponents: [i32; 3], + /// Render cell coefficient for the smallest visible gridlines. + grid_coefficient: f32, + /// Exponential base for gridlines. + grid_base: i32, + + /// High end of the gridline opacity gradient; pixel spacing for gridlines + /// with maximum opacity. + grid_max_spacing: f32, + /// Low end of the gridline opacity gradient; pixel spacing for gridlines + /// with zero opacity. + grid_min_spacing: f32, +} + +implement_uniform_block!( + GridlineParams, + grid_color, + grid_origin, + grid_width, + grid_max_exponents, + grid_coefficient, + grid_base, + grid_max_spacing, + grid_min_spacing, +); + +impl From<&GridViewRender3D<'_>> for GridlineParams { + fn from(gvr3d: &GridViewRender3D<'_>) -> Self { + // Compute the coefficient for the smallest visible gridlines. + let log2_global_min_spacing = (GRIDLINE_SPACING_BASE as f32).log2() + * (gvr3d.gridline_cell_spacing_exponent(1.0) as f32) + + (GRIDLINE_SPACING_COEFF as f32).log2(); + let global_min_spacing = FixedPoint::from_f32(log2_global_min_spacing) + .unwrap() + .exp2() + .round(); + let log2_local_min_spacing = + log2_global_min_spacing - (gvr3d.xform.render_cell_layer.to_u32() as f32); + let local_coefficient = log2_local_min_spacing.exp2(); + + // Find the position of a gridline in or near the visible area with the + // largest exponent. + let global_gridline_origin: BigVec3D; + { + // Compute the largest gridline spacing that fits within the visible + // area. + let max_visible_exponent = + gvr3d.gridline_cell_spacing_exponent(Viewpoint3D::VIEW_RADIUS as f64 * 2.0); + let max_visible_spacing = BigInt::from(GRIDLINE_SPACING_BASE) + .pow(max_visible_exponent + 1) + * GRIDLINE_SPACING_COEFF; + // Round to nearest multiple of that spacing. + global_gridline_origin = + gvr3d.xform.origin.div_floor(&max_visible_spacing) * &max_visible_spacing; + } + + // Compute the maximum exponent that will be visible for each axis. + // There is a similar loop in the 3D gridlines fragment shader. + let mut max_exponents = IVec3D::repeat(0); + for &ax in Dim3D::axes() { + const LARGE_EXPONENT: isize = 16; + let spacing_base: BigInt = GRIDLINE_SPACING_BASE.into(); + + let mut tmp: BigInt = global_gridline_origin[ax].div_floor(&global_min_spacing); + if tmp.is_zero() { + max_exponents[ax] = LARGE_EXPONENT; + } else { + while tmp.mod_floor(&spacing_base).is_zero() && max_exponents[ax] < LARGE_EXPONENT { + tmp /= GRIDLINE_SPACING_BASE; + max_exponents[ax] += 1; + } + } + } + + let local_gridline_origin = gvr3d + .xform + .global_to_local_float(&global_gridline_origin.to_fixedvec()) + .unwrap(); + + let line_width = if gvr3d.xform.render_cell_layer == Layer(0) { + GRIDLINE_WIDTH as f32 + } else { + 0.0 // minimum width of one pixel + }; + + Self { + grid_color: crate::colors::GRIDLINES.into_raw(), + grid_width: line_width, + + grid_origin: local_gridline_origin.to_f32_array(), + grid_max_exponents: max_exponents.to_i32_array(), + grid_coefficient: local_coefficient, + grid_base: GRIDLINE_SPACING_BASE as i32, + + grid_min_spacing: GRIDLINE_ALPHA_GRADIENT_LOW_PIXEL_SPACING as f32, + grid_max_spacing: GRIDLINE_ALPHA_GRADIENT_HIGH_PIXEL_SPACING as f32, + } + } +} diff --git a/ui/src/gridview/render/render3d/lighting.rs b/ui/src/gridview/render/render3d/lighting.rs new file mode 100644 index 00000000..4c4f467c --- /dev/null +++ b/ui/src/gridview/render/render3d/lighting.rs @@ -0,0 +1,25 @@ +use super::*; + +#[derive(Debug, Copy, Clone)] +pub(super) struct LightingParams { + light_direction: [f32; 3], + light_ambientness: f32, + light_multiplier: f32, +} + +implement_uniform_block!( + LightingParams, + light_direction, + light_ambientness, + light_multiplier, +); + +impl Default for LightingParams { + fn default() -> Self { + Self { + light_direction: LIGHT_DIRECTION, + light_ambientness: LIGHT_AMBIENTNESS, + light_multiplier: LIGHT_MULTIPLIER, + } + } +} diff --git a/ui/src/gridview/render/render3d/mod.rs b/ui/src/gridview/render/render3d/mod.rs new file mode 100644 index 00000000..cc039bc5 --- /dev/null +++ b/ui/src/gridview/render/render3d/mod.rs @@ -0,0 +1,747 @@ +//! 3D grid rendering. +//! +//! Currently, only solid colors are supported, however I plan to add custom +//! models and maybe textures in the future. + +use anyhow::{Context, Result}; +use glium::glutin::event::ModifiersState; +use glium::index::PrimitiveType; +use glium::uniforms::UniformBuffer; +use glium::Surface; +use itertools::Itertools; +use palette::{Pixel, Srgb, Srgba}; +use sloth::Lazy; + +use ndcell_core::prelude::*; + +mod fog; +mod gridlines; +mod lighting; + +use super::consts::*; +use super::generic::{GenericGridViewRender, GridViewRenderDimension, LineEndpoint3D, OverlayFill}; +use super::shaders; +use super::vertices::Vertex3D; +use super::CellDrawParams; +use crate::commands::{DragCmd, DrawMode}; +use crate::ext::*; +use crate::gridview::*; +use crate::{Face, CONFIG, DISPLAY, FACES}; +use fog::FogParams; +use gridlines::GridlineParams; +use lighting::LightingParams; + +pub(in crate::gridview) type GridViewRender3D<'a> = GenericGridViewRender<'a, RenderDim3D>; + +type QuadVerts = [Vertex3D; 4]; +type CuboidVerts = [Option; 6]; + +type LazyUbo = Lazy, Box UniformBuffer>>; + +pub(in crate::gridview) struct RenderDim3D { + fog_uniform: LazyUbo, + gridline_uniform: LazyUbo, + lighting_uniform: LazyUbo, +} +impl<'a> GridViewRenderDimension<'a> for RenderDim3D { + type D = Dim3D; + type Viewpoint = Viewpoint3D; + type OverlayQuad = OverlayQuad; + + const DEFAULT_COLOR: Srgb = crate::colors::BACKGROUND_3D; + const DEFAULT_DEPTH: f32 = f32::INFINITY; + const LINE_MIN_PIXEL_WIDTH: f64 = LINE_MIN_PIXEL_WIDTH_3D; + + fn init(gvr3d: &GridViewRender3D<'a>) -> Self { + let fog_params = FogParams::from(gvr3d); + let gridline_params = GridlineParams::from(gvr3d); + let lighting_params = LightingParams::default(); + + fn lazy_ubo(u: U) -> LazyUbo { + Lazy::new(Box::new(move || { + UniformBuffer::new(&**DISPLAY, u).expect("Failed to create uniform buffer") + })) + } + + Self { + fog_uniform: lazy_ubo(fog_params), + gridline_uniform: lazy_ubo(gridline_params), + lighting_uniform: lazy_ubo(lighting_params), + } + } + + fn draw_overlay_quads(this: &mut GridViewRender3D<'a>) -> Result<()> { + this.cull_backface_quads(); + this.depth_sort_quads(); + + let gl_matrix = this.xform.gl_matrix(); + + let fog_params = &**this.dim.as_ref().unwrap().fog_uniform; + let lighting_params = &**this.dim.as_ref().unwrap().lighting_uniform; + let gridline_params = &**this.dim.as_ref().unwrap().gridline_uniform; + + // Reborrow is necessary in order to split borrow. + let cache = &mut *this.cache; + let vbos = &mut cache.vbos; + let ibos = &mut cache.ibos; + + // `group_by()` is defined using an extension trait in this crate; it is + // semantically identical to `Itertools::group_by()`, but operates on a + // slice instead of an iterator. `chunks()` is part of the standard + // library. + + let mut verts = vec![]; + // Group by what shader to use and whether the quad is opaque ... + for ((shader_program, is_opaque), quads_group) in this + .overlay_quads + .group_by(|quad| (quad.shader_program(), quad.is_opaque())) + { + // ... and then process these in batches, because the VBO has + // limited capacity. + for chunk in quads_group.chunks(QUAD_BATCH_SIZE) { + // Generate vertices. + verts.clear(); + for quad in chunk { + verts.extend_from_slice(&quad.verts()); + } + + // Populate VBO and IBO. + let quad_count = chunk.len(); + let vbo = vbos.quad_verts_3d(quad_count); + vbo.write(&verts); + let ibo = ibos.quad_indices(quad_count); + + // Draw quads. + this.params + .target + .draw( + vbo, + &ibo, + &shader_program.load(), + &uniform! { + matrix: gl_matrix, + + FogParams: fog_params, + LightingParams: lighting_params, + GridlineParams: gridline_params, + }, + &glium::DrawParameters { + blend: glium::Blend::alpha_blending(), + depth: glium::Depth { + test: glium::DepthTest::IfLessOrEqual, + write: is_opaque, + ..Default::default() + }, + ..Default::default() + }, + ) + .context("Drawing 3D overlay quads")?; + } + } + Ok(()) + } +} + +impl GridViewRender3D<'_> { + /// Draw an ND-tree to scale on the target. + pub fn draw_cells(&mut self, params: CellDrawParams<'_, Dim3D>) -> Result<()> { + let visible_octree = match self.clip_ndtree_to_visible(¶ms) { + Some(x) => x, + None => return Ok(()), // There is nothing to draw. + }; + + let octree_base = self + .xform + .global_to_local_int(&visible_octree.base_pos) + .unwrap(); + + // Reborrow is necessary in order to split borrow. + let cache = &mut *self.cache; + let vbos = &mut cache.vbos; + + let gl_octree = cache + .gl_octrees + .gl_ndtree_from_node((&visible_octree.root).into(), self.xform.render_cell_layer)?; + + self.params + .target + .draw( + &*vbos.ndtree_quad(), + &glium::index::NoIndices(PrimitiveType::TriangleStrip), + &shaders::OCTREE.load(), + &uniform! { + matrix: self.xform.gl_matrix(), + + octree_texture: &gl_octree.texture, + layer_count: gl_octree.layers, + root_idx: gl_octree.root_idx, + + octree_base: octree_base.to_i32_array(), + + perf_view: CONFIG.lock().gfx.octree_perf_view, + + alpha: params.alpha, + + FogParams: &**self.dim.as_ref().unwrap().fog_uniform, + LightingParams: &**self.dim.as_ref().unwrap().lighting_uniform, + }, + &glium::DrawParameters { + depth: glium::Depth { + test: glium::DepthTest::IfLessOrEqual, + write: true, + ..Default::default() + }, + blend: glium::Blend::alpha_blending(), + multisampling: false, + ..Default::default() + }, + ) + .expect("Drawing cells"); + + // If the mouse is hovering over a cell, and these cells are + // interactive, draw that on the mouse picker. + if params.interactive { + if let Some(pixel) = self.params.mouse.pos { + let (start, delta) = self.xform.pixel_to_local_ray(pixel); + let raycast = crate::gridview::algorithms::raycast::intersect_octree( + start - octree_base.to_fvec(), + delta, + self.xform.render_cell_layer, + visible_octree.root.as_ref(), + ); + if let Some(hit) = raycast { + let cuboid = IRect::single_cell(hit.pos_int + octree_base).to_frect(); + let face = hit.face; + let modifiers = None; + let target_data = None; + self.add_mouse_target_face(cuboid, face, modifiers, target_data); + } + } + } + + Ok(()) + } + + /// Adds a plane of gridlines to the overlay. + pub fn add_gridlines_overlay( + &mut self, + perpendicular_axis: Axis, + perpendicular_coordinate: BigInt, + ) { + let local_perpendicular_coordinate; + if let Some(local_coord) = self + .xform + .global_to_local_visible_coord(perpendicular_axis, &perpendicular_coordinate) + { + local_perpendicular_coordinate = r64(local_coord as f64); + } else { + return; // The gridline plane isn't even visible. + }; + + // Compute rectangle. + let rect: FRect3D; + { + let mut min = self.local_visible_rect.min().to_fvec(); + let mut max = self.local_visible_rect.max().to_fvec(); + min[perpendicular_axis] = local_perpendicular_coordinate - Z_EPSILON; + max[perpendicular_axis] = local_perpendicular_coordinate + Z_EPSILON; + rect = NdRect::span(min, max); + } + + self.overlay_quads.push(OverlayQuad { + rect, + face: Face::positive(perpendicular_axis), + fill: OverlayFill::Gridlines3D, + }); + self.overlay_quads.push(OverlayQuad { + rect, + face: Face::negative(perpendicular_axis), + fill: OverlayFill::Gridlines3D, + }); + } + + /// Adds a highlight on the render cell face under the mouse cursor when + /// using the drawing tool. + pub fn add_hover_draw_overlay(&mut self, cell_pos: &BigVec3D, face: Face, draw_mode: DrawMode) { + self.add_hover_overlay( + cell_pos, + face, + draw_mode.fill_color(), + draw_mode.outline_color(), + ); + } + /// Adds a highlight on the render cell face under the mouse cursor when + /// using the selection tool. + pub fn add_hover_select_overlay(&mut self, cell_pos: &BigVec3D, face: Face) { + use crate::colors::hover::*; + self.add_hover_overlay(cell_pos, face, SELECT_FILL, SELECT_OUTLINE); + } + /// Adds a highlight on the render cell under the mouse cursor. + fn add_hover_overlay( + &mut self, + cell_pos: &BigVec3D, + face: Face, + fill_color: Srgba, + outline_color: Srgb, + ) { + let local_rect = IRect::single_cell(self.clamp_int_pos_to_visible(cell_pos)); + let local_frect = self._adjust_rect_for_overlay(local_rect); + let width = r64(HOVER_HIGHLIGHT_WIDTH); + self.add_cuboid_fill_overlay(local_frect, fill_color); + self.add_face_outline_overlay(local_rect, face, outline_color, width); + } + + /// Adds a highlight around the selection when the selection includes cells. + pub fn add_selection_cells_highlight_overlay(&mut self, selection_rect: &BigRect3D) { + use crate::colors::selection::*; + self.add_selection_highlight_overlay(selection_rect, CELLS_FILL, CELLS_OUTLINE) + } + /// Adds a highlight around the selection when the selection does not + /// include cells. + pub fn add_selection_region_highlight_overlay(&mut self, selection_rect: &BigRect3D) { + use crate::colors::selection::*; + self.add_selection_highlight_overlay(selection_rect, REGION_FILL, REGION_OUTLINE) + } + /// Adds a highlight around the selection. + fn add_selection_highlight_overlay( + &mut self, + selection_rect: &BigRect3D, + fill_color: Srgba, + outline_color: Srgb, + ) { + let local_rect = self.clip_int_rect_to_visible(selection_rect); + let local_frect = self._adjust_rect_for_overlay(local_rect); + let width = r64(SELECTION_HIGHLIGHT_WIDTH); + self.add_cuboid_fill_overlay(local_frect, fill_color); + self.add_cuboid_outline_overlay(local_rect, outline_color, width); + + for &face in &FACES { + // "Move selected cells" target. + let binding = DragCmd::MoveSelectedCells(Some(face)); + self.add_mouse_target_face( + local_frect, + face, + Some(ModifiersState::empty()), + Some(MouseTargetData { binding }), + ); + + // "Move selection" target. + let binding = DragCmd::MoveSelection(Some(face)); + self.add_mouse_target_face( + local_frect, + face, + Some(ModifiersState::SHIFT), + Some(MouseTargetData { binding }), + ); + + // "Move copy of cells" target. + let binding = DragCmd::CopySelectedCells(Some(face)); + self.add_mouse_target_face( + local_frect, + face, + Some(ModifiersState::CTRL), + Some(MouseTargetData { binding }), + ); + + // "Resize selection" target. + let binding = DragCmd::ResizeSelection3D(face); + self.add_mouse_target_face( + local_frect, + face, + Some(ModifiersState::CTRL | ModifiersState::SHIFT), + Some(MouseTargetData { binding }), + ) + } + } + /// Adds a highlight indicating how the selection will be resized. + pub fn add_selection_resize_preview_overlay(&mut self, selection_preview_rect: &BigRect3D) { + let local_rect = self.clip_int_rect_to_visible(selection_preview_rect); + let local_frect = self._adjust_rect_for_overlay(local_rect); + let width = r64(SELECTION_HIGHLIGHT_WIDTH * WIDTH_FUDGE_FACTOR_3D); + use crate::colors::selection::*; + self.add_cuboid_fill_overlay(local_frect, RESIZE_FILL); + self.add_cuboid_outline_overlay(local_rect, RESIZE_OUTLINE, width); + } + /// Adds a highlight indicating which face of the selection will be resized. + pub fn add_selection_face_resize_overlay(&mut self, selection_rect: &BigRect3D, face: Face) { + let local_rect = self.clip_int_rect_to_visible(selection_rect); + let local_frect = self._adjust_rect_for_overlay(local_rect); + let width = r64(SELECTION_HIGHLIGHT_WIDTH * WIDTH_FUDGE_FACTOR_3D); + let fill_color = crate::colors::selection::RESIZE_FILL; + let outline_color = crate::colors::selection::RESIZE_OUTLINE; + self.add_face_fill_overlay(local_frect, face, fill_color); + self.add_back_face_fill_overlay(local_frect, face, fill_color); + self.add_face_outline_overlay(local_rect, face, outline_color, width); + } + + /// Adds all six faces of a filled-in cuboid with a solid color. + fn add_cuboid_fill_overlay(&mut self, cuboid: FRect3D, fill: impl Copy + Into) { + let fill = fill.into(); + for &face in &FACES { + self.add_face_fill_overlay(cuboid, face, fill); + if !fill.is_opaque() { + self.add_back_face_fill_overlay(cuboid, face, fill); + } + } + } + /// Adds an outline around all faces of a cuboid with a solid color. + fn add_cuboid_outline_overlay(&mut self, cuboid: IRect3D, color: Srgb, width: R64) { + let cuboid = cuboid.to_frect(); + for &ax in Dim3D::axes() { + let neg_corners = Face::negative(ax).corners_of(cuboid); + let pos_corners = Face::positive(ax).corners_of(cuboid); + for &(i1, i2) in &[(0, 0), (1, 2), (2, 1), (3, 3)] { + self.add_line_overlay( + LineEndpoint3D::include(neg_corners[i1], color), + LineEndpoint3D::include(pos_corners[i2], color), + width, + ); + } + } + } + /// Adds a filled-in face of a cuboid with a solid color. + fn add_face_fill_overlay(&mut self, cuboid: FRect3D, face: Face, fill: impl Into) { + let rect = face.of(cuboid); + let fill = fill.into(); + self.overlay_quads.push(OverlayQuad { rect, face, fill }); + } + /// Adds a filled-in back-face of a cuboid with a solid color. + fn add_back_face_fill_overlay( + &mut self, + cuboid: FRect3D, + face: Face, + fill: impl Into, + ) { + let rect = face.of(cuboid); + let face = face.opposite(); + let fill = fill.into(); + self.overlay_quads.push(OverlayQuad { rect, face, fill }); + } + /// Adds an outline around a face of a cuboid with a solid color. + fn add_face_outline_overlay(&mut self, cuboid: IRect3D, face: Face, color: Srgb, width: R64) { + let rect = face.of(cuboid.to_frect()); + let min = rect.min(); + let max = rect.max(); + let [ax1, ax2] = face.plane_axes(); + let mut corners = [min; 5]; + corners[1][ax1] = max[ax1]; + corners[2][ax1] = max[ax1]; + corners[2][ax2] = max[ax2]; + corners[3][ax2] = max[ax2]; + + for (&c1, &c2) in corners.iter().tuple_windows() { + self.add_line_overlay( + LineEndpoint3D::include(c1, color), + LineEndpoint3D::include(c2, color), + width, + ); + } + } + /// Adds crosshairs around a face of a cuboid with a solid color that fades into + /// the gridline color toward the edges. + fn add_face_crosshairs_overlay( + &mut self, + cuboid: IRect3D, + face: Face, + color: Srgb, + width: R64, + ) { + let rect = face.of(cuboid.to_frect()); + let min = rect.min(); + let max = rect.max(); + let [ax1, ax2] = face.plane_axes(); + let a1 = min[ax1]; + let a2 = min[ax2]; + let b1 = max[ax1]; + let b2 = max[ax2]; + + self.add_single_crosshair_overlay(ax1, a1, b1, min, width, color); // bottom + self.add_single_crosshair_overlay(ax1, a1, b1, max, width, color); // top + self.add_single_crosshair_overlay(ax2, a2, b2, min, width, color); // left + self.add_single_crosshair_overlay(ax2, a2, b2, max, width, color); // right + } + fn add_single_crosshair_overlay( + &mut self, + parallel_axis: Axis, + a: R64, + b: R64, + position: FVec3D, + width: R64, + color: Srgb, + ) { + let endpoint_pairs = self.make_crosshair_endpoints(parallel_axis, a, b, position, color); + for [start, end] in endpoint_pairs { + self.add_line_overlay(start, end, width); + } + } + fn add_line_overlay(&mut self, mut start: LineEndpoint3D, mut end: LineEndpoint3D, width: R64) { + let (rect, axis) = self.make_line_ndrect(&mut start, &mut end, width); + let fill = OverlayFill::gradient(axis, start.color, end.color); + self.add_cuboid_fill_overlay(rect, fill); + } + + /// Draws many transparent intersecting planes of different colors to test + /// transparent splitting and sorting. + pub fn draw_transparency_test(&mut self) { + let min = NdVec([0, 0, 5]); + let max = NdVec([5, 5, 10]); + + for &ax in Dim3D::axes() { + for coord in (min[ax] + 1)..(max[ax] + 1) { + let mut min = min; + let mut max = max; + min[ax] = coord; + max[ax] = coord; + let global_rect = IRect3D::span(min, max).to_bigrect(); + let rect = self.clip_int_rect_to_visible(&global_rect).to_frect(); + let r = (ax == Axis::X) as u8 as f32; + let g = (ax == Axis::Y) as u8 as f32; + let b = (ax == Axis::Z) as u8 as f32; + self.overlay_quads.push(OverlayQuad { + rect, + face: Face::positive(ax), + fill: OverlayFill::Solid(Srgba::new(r, g, b, 0.25)), + }); + self.overlay_quads.push(OverlayQuad { + rect, + face: Face::negative(ax), + fill: OverlayFill::Solid(Srgba::new(r, g, b, 0.25)), + }); + } + } + } + + /// Remove quads that are not visible from the current camera position. + fn cull_backface_quads(&mut self) { + // Get global camera positiion. + let camera_pos = self.viewpoint.camera_pos(); + // Get local camera position. + let camera_pos = self.xform.global_to_local_float(&camera_pos).unwrap(); + // Cull those quads! + self.overlay_quads + .retain(|quad| quad.is_visible_from(camera_pos)); + } + /// Sort quads by depth, splitting as necessary. + fn depth_sort_quads(&mut self) { + let transparent_quads = self + .overlay_quads + .iter() + .copied() + .filter(|quad| !quad.is_opaque()) + .collect_vec(); + + let x_quads = Self::filter_and_sort_quads_on_axis(&transparent_quads, Axis::X); + let y_quads = Self::filter_and_sort_quads_on_axis(&transparent_quads, Axis::Y); + let z_quads = Self::filter_and_sort_quads_on_axis(&transparent_quads, Axis::Z); + + let mut sorted_transparent_quads = x_quads; + sorted_transparent_quads = + Self::split_and_intersperse_quads(sorted_transparent_quads, &y_quads); + sorted_transparent_quads = + Self::split_and_intersperse_quads(sorted_transparent_quads, &z_quads); + + // Draw all opaque quads first, then all the transparent quads, + // back-to-front. + self.overlay_quads.retain(|quad| quad.is_opaque()); + self.overlay_quads + .extend_from_slice(&sorted_transparent_quads); + } + /// Filters the quads to only those with a particular normal axis, then sort + /// those quads back-to-front. + fn filter_and_sort_quads_on_axis(quads: &[OverlayQuad], axis: Axis) -> Vec { + quads + .iter() + .copied() + .filter(|q| q.face.normal_axis() == axis) + .sorted_by_key(|q| q.plane()) + .collect_vec() + } + /// Splits the quads in the first list at each intersection with a quad from + /// the second list, and sort all quads back-to-front. Both initial lists + /// must be initially sorted back-to-front. + fn split_and_intersperse_quads( + quads: Vec, + splits: &[OverlayQuad], + ) -> Vec { + let mut ret = vec![]; + let mut remaining_in_front = quads; + for &split_quad in splits { + remaining_in_front = remaining_in_front + .into_iter() + .filter_map(|remaining_quad| { + let [behind, in_front] = remaining_quad.split_at_plane(split_quad.plane()); + ret.extend(behind); + in_front + }) + .collect_vec(); + ret.push(split_quad); + } + ret.extend_from_slice(&remaining_in_front); + ret + } + + fn add_mouse_target_face( + &mut self, + cuboid: FRect3D, + face: Face, + modifiers: Option, + data: Option, + ) { + let corners = face.corners_of(cuboid); + let target_id = data.map(|data| self.add_mouse_target(data)).unwrap_or(0); + self.add_mouse_target_tri(modifiers, [corners[0], corners[1], corners[2]], target_id); + self.add_mouse_target_tri(modifiers, [corners[3], corners[2], corners[1]], target_id); + } + + fn _adjust_rect_for_overlay(&self, rect: IRect3D) -> FRect3D { + // Avoid Z-fighting with cells by expanding the rectangle a tiny bit. + rect.to_frect() + .offset_min_max(r64(-CUBOID_OVERLAY_PADDING), r64(CUBOID_OVERLAY_PADDING)) + } +} + +/// Simple rectangle in a cell overlay. +#[derive(Debug, Copy, Clone)] +pub struct OverlayQuad { + /// 3D region; all coordinates must be in `face` plane. + rect: FRect3D, + /// Facing direction of the quad. + face: Face, + /// Fill (determines color and shader program). + fill: OverlayFill, +} +impl OverlayQuad { + /// Returns `true` if the quad is definitely 100% opaque. + pub fn is_opaque(self) -> bool { + self.fill.is_opaque() + } + /// Returns `true` if the front of the quad faces the camera, or `false` if + /// the back of the quad faces the camera. + pub fn is_visible_from(self, camera_pos: FVec3D) -> bool { + let quad_coordinate = self.perpendicular_coordinate(); + let camera_coordinate = camera_pos[self.face.normal_axis()]; + match self.face.sign() { + Sign::Minus => camera_coordinate < quad_coordinate, + Sign::NoSign => unreachable!(), + Sign::Plus => camera_coordinate > quad_coordinate, + } + } + + /// Returns the infinite plane that the quad is inside. + pub fn plane(self) -> SignedPlane { + SignedPlane { + face: self.face, + coordinate: self.perpendicular_coordinate(), + } + } + /// Returns the coordinate of the face along its normal axis. + pub fn perpendicular_coordinate(self) -> R64 { + self.rect.min()[self.face.normal_axis()] + } + + /// Splits the quad at the given plane, returning the portion behind the + /// plane and the portion in front of the plane in that order. + pub fn split_at_plane(self, plane: SignedPlane) -> [Option; 2] { + let split_axis = plane.face.normal_axis(); + let split_coordinate = plane.coordinate; + + let mut negative_side = None; + let mut positive_side = None; + if self.face.normal_axis() == split_axis { + match self.perpendicular_coordinate().cmp(&split_coordinate) { + std::cmp::Ordering::Less => negative_side = Some(self), + std::cmp::Ordering::Equal => return [None, Some(self)], // preserve original order + std::cmp::Ordering::Greater => positive_side = Some(self), + } + } else { + let min = self.rect.min(); + let max = self.rect.max(); + if max[split_axis] <= split_coordinate { + negative_side = Some(self); + } else if min[split_axis] >= split_coordinate { + positive_side = Some(self); + } else { + // We actually have to split the rectangle. + + let mut neg_max = max; + neg_max[split_axis] = split_coordinate; + negative_side = Some(Self { + rect: NdRect::span(min, neg_max), + face: self.face, + fill: self.fill, + }); + + let mut pos_min = min; + pos_min[split_axis] = split_coordinate; + positive_side = Some(Self { + rect: NdRect::span(pos_min, max), + face: self.face, + fill: self.fill, + }); + } + } + + match plane.face.sign() { + Sign::Minus => [positive_side, negative_side], + Sign::NoSign => unreachable!(), + Sign::Plus => [negative_side, positive_side], + } + } + + /// Returns the four vertices to render the quad. + pub fn verts(self) -> [Vertex3D; 4] { + let [ax1, ax2] = self.face.plane_axes(); + let min = self.rect.min(); + let max = self.rect.max(); + let mut positions = [min; 4]; + positions[1][ax1] = max[ax1]; + positions[2][ax2] = max[ax2]; + positions[3][ax1] = max[ax1]; + positions[3][ax2] = max[ax2]; + + let normal = self.face.normal(); + let colors = self.fill.vertex_colors(self.face); + [ + Vertex3D::new(positions[0].to_f32_array(), normal, colors[0]), + Vertex3D::new(positions[1].to_f32_array(), normal, colors[1]), + Vertex3D::new(positions[2].to_f32_array(), normal, colors[2]), + Vertex3D::new(positions[3].to_f32_array(), normal, colors[3]), + ] + } + /// Returns the shader program that should be used to render the quad: + /// either `RGBA_3D` or `GRIDLINES`. + pub fn shader_program(self) -> &'static shaders::WrappedShader { + match self.fill { + OverlayFill::Solid(_) | OverlayFill::Gradient(_, _, _) => &*shaders::RGBA_3D, + OverlayFill::Gridlines3D => &*shaders::GRIDLINES_3D, + } + } +} + +/// Axis-aligned plane in local space. +/// +/// A total ordering is defined on this type that is guaranteed to sort parallel +/// planes in back-to-front draw order. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct SignedPlane { + pub face: Face, + pub coordinate: R64, +} +impl PartialOrd for SignedPlane { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Ord for SignedPlane { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.face + .normal_axis() + .cmp(&other.face.normal_axis()) + .then(self.face.sign().cmp(&other.face.sign())) + .then_with(|| match self.face.sign() { + Sign::Minus => self.coordinate.cmp(&other.coordinate).reverse(), + Sign::NoSign => unreachable!(), + Sign::Plus => self.coordinate.cmp(&other.coordinate), + }) + } +} diff --git a/ui/src/gridview/render/shaders/gridlines_3d.frag b/ui/src/gridview/render/shaders/gridlines_3d.frag index e7363bd7..c5a9986e 100644 --- a/ui/src/gridview/render/shaders/gridlines_3d.frag +++ b/ui/src/gridview/render/shaders/gridlines_3d.frag @@ -1,19 +1,24 @@ -#version 140 +#version 150 in vec3 pos_3d; in vec2 grid_pos; out vec4 color; -uniform vec4 grid_color; +layout(std140) uniform GridlineParams { + vec4 grid_color; -uniform int grid_base; // exponential base -uniform ivec2 grid_max_exponents; + vec3 grid_origin; -uniform float min_line_spacing; -uniform float max_line_spacing; + float grid_width; // measured as a fraction of a render cell -uniform float line_width; // measured as a fraction of a render cell unit + ivec3 grid_max_exponents; + float grid_coefficient; + int grid_base; // exponential base + + float grid_max_spacing; + float grid_min_spacing; +}; // This line includes another file in this GLSL program. See `shaders/mod.rs`. //#include util/fog.frag @@ -62,13 +67,13 @@ void main() { // The gridline must be at least one pixel thick. vec2 delta = fwidth(grid_pos); // d(grid pos)/d(pixel pos) - vec2 line_width = max(vec2(line_width), delta); + vec2 line_width = max(vec2(grid_width), delta); - // Convert `min`/`max_line_spacing` from pixels to cells, and then take the + // Convert `grid_(min|max)_spacing` from pixels to cells, and then take the // `grid_base`th root (which we can do using division in logarithm land). float log2_grid_base = log2(grid_base); - vec2 min_exponent = log2(min_line_spacing * delta) / log2_grid_base; - vec2 max_exponent = log2(max_line_spacing * delta) / log2_grid_base; + vec2 min_exponent = log2(grid_min_spacing * delta) / log2_grid_base; + vec2 max_exponent = log2(grid_max_spacing * delta) / log2_grid_base; // Compute the exponent of the smallest possible gridline that would be // visible. The exponent must be positive, because we never show gridlines diff --git a/ui/src/gridview/render/shaders/gridlines_3d.vert b/ui/src/gridview/render/shaders/gridlines_3d.vert index 24316c1c..8eb90472 100644 --- a/ui/src/gridview/render/shaders/gridlines_3d.vert +++ b/ui/src/gridview/render/shaders/gridlines_3d.vert @@ -1,29 +1,42 @@ -#version 140 +#version 150 -in vec2 pos; +in vec3 pos; +in vec3 normal; +in vec4 color; // ignored -out vec3 pos_3d; +out vec3 pos_3d; // XYZ coordinates in local space out vec2 grid_pos; // XY coordinates on 2D grid uniform mat4 matrix; -uniform ivec2 grid_axes; -uniform vec3 grid_origin; // origin for gridlines -uniform float grid_coefficient; +layout(std140) uniform GridlineParams { + vec4 grid_color; -const float epsilon = 1.0 / 256.0; + vec3 grid_origin; + + float grid_width; // measured as a fraction of a render cell + + ivec3 grid_max_exponents; + float grid_coefficient; + int grid_base; // exponential base + + float grid_max_spacing; + float grid_min_spacing; +}; + +const float EPSILON = 1.0 / 256.0; void main() { - int ax1 = grid_axes.x; - int ax2 = grid_axes.y; + gl_Position = matrix * vec4(pos, 1.0); + gl_Position.z -= EPSILON; // Gridlines appear in front of cells. - pos_3d = grid_origin; - pos_3d[ax1] = pos.x; - pos_3d[ax2] = pos.y; + // Identify the axes that are parallel to the plane. + int ax1; if (normal.x == 0.0) { ax1 = 0; } else { ax1 = 1; } + int ax2; if (normal.z == 0.0) { ax2 = 2; } else { ax2 = 1; } - gl_Position = matrix * vec4(pos_3d, 1.0); - gl_Position.z -= epsilon; // Gridlines appear in front of cells. + pos_3d = pos; - vec2 grid_origin = vec2(grid_origin[ax1], grid_origin[ax2]); - grid_pos = (pos - grid_origin) / grid_coefficient; + vec2 pos_2d = vec2(pos[ax1], pos[ax2]); + vec2 origin_2d = vec2(grid_origin[ax1], grid_origin[ax2]); + grid_pos = (pos_2d - origin_2d) / grid_coefficient; } diff --git a/ui/src/gridview/render/shaders/mod.rs b/ui/src/gridview/render/shaders/mod.rs index 21e6f345..336ba9ee 100644 --- a/ui/src/gridview/render/shaders/mod.rs +++ b/ui/src/gridview/render/shaders/mod.rs @@ -14,15 +14,15 @@ mod wrapped; use wrapped::*; #[cfg(debug_assertions)] -type WrappedShader = DynamicWrappedShader; +pub type WrappedShader = DynamicWrappedShader; #[cfg(not(debug_assertions))] -type WrappedShader = StaticWrappedShader; +pub type WrappedShader = StaticWrappedShader; shader!(RGBA_2D = { srgb: true, "rgba_2d" }); shader!(RGBA_3D = { srgb: true, "rgba_3d" }); -shader!(QUADTREE = { srgb: true, vert: "screen_pos", frag: "quadtree" }); -shader!(OCTREE = { srgb: true, vert: "screen_pos", frag: "octree" }); +shader!(QUADTREE = { srgb: true, vert: "ndc_xy", frag: "quadtree" }); +shader!(OCTREE = { srgb: true, vert: "ndc_xy", frag: "octree" }); shader!(GRIDLINES_3D = { srgb: true, "gridlines_3d" }); diff --git a/ui/src/gridview/render/shaders/ndc_xy.vert b/ui/src/gridview/render/shaders/ndc_xy.vert new file mode 100644 index 00000000..96b83a19 --- /dev/null +++ b/ui/src/gridview/render/shaders/ndc_xy.vert @@ -0,0 +1,10 @@ +#version 150 + +in vec2 pos; + +out vec2 ndc_xy; // normalized device coordinates XY + +void main() { + gl_Position = vec4(pos, 0.0, 1.0); + ndc_xy = pos; +} diff --git a/ui/src/gridview/render/shaders/octree.frag b/ui/src/gridview/render/shaders/octree.frag index 5c014dee..5c09b92a 100644 --- a/ui/src/gridview/render/shaders/octree.frag +++ b/ui/src/gridview/render/shaders/octree.frag @@ -1,9 +1,9 @@ // Octree traversal algorithm based on An Efficient Parametric Algorithm for // Octree Traversal by J. Revelles, C. Ureña, M. Lastra. -#version 140 +#version 150 -in vec2 screen_pos; // -1.0 ... +1.0 (same as `gl_Position`) +in vec2 ndc_xy; // -1.0 ... +1.0 (same as `gl_Position`) out vec4 color; @@ -15,12 +15,14 @@ uniform usampler2D octree_texture; uniform int layer_count; uniform uint root_idx; -uniform ivec3 octree_offset; +uniform ivec3 octree_base; uint texture_width = uint(textureSize(octree_texture, 0).x); float octree_side_len = (1 << layer_count); uniform bool perf_view; +uniform float alpha; + // These lines include other files in this GLSL program. See `shaders/mod.rs`. //#include util/fog.frag //#include util/lighting.frag @@ -91,7 +93,7 @@ void main() { // https://stackoverflow.com/a/42634961) vec3 start, delta; { - vec4 near = inv_matrix * vec4(screen_pos, 0.0, 1.0); + vec4 near = inv_matrix * vec4(ndc_xy, 0.0, 1.0); vec4 far = near + inv_matrix[2]; near.xyz /= near.w; far.xyz /= far.w; @@ -108,7 +110,7 @@ void main() { // positive and also mirror the quadtree along that axis using // `invert_mask`, which will flip bits of child indices. bvec3 invert_mask = lessThan(delta, vec3(0)); - start -= octree_offset; + start -= octree_base; start = mix(start, octree_side_len - start, invert_mask); delta = mix(delta, -delta, invert_mask); @@ -213,7 +215,7 @@ void main() { float g = float((child_value >> 16) & 255u) / 255.0; float b = float((child_value >> 8) & 255u) / 255.0; float a = float( child_value & 255u) / 255.0; - color = vec4(r, g, b, a); + color = vec4(r, g, b, a * alpha); // Compute lighting using a normal vector based on the entry // axis for the node. diff --git a/ui/src/gridview/render/shaders/picker.frag b/ui/src/gridview/render/shaders/picker.frag index 9ea69c06..c8f2875b 100644 --- a/ui/src/gridview/render/shaders/picker.frag +++ b/ui/src/gridview/render/shaders/picker.frag @@ -1,4 +1,4 @@ -#version 140 +#version 150 flat in uint vId; diff --git a/ui/src/gridview/render/shaders/picker.vert b/ui/src/gridview/render/shaders/picker.vert index bdef5b0f..dbcd42d8 100644 --- a/ui/src/gridview/render/shaders/picker.vert +++ b/ui/src/gridview/render/shaders/picker.vert @@ -1,4 +1,4 @@ -#version 140 +#version 150 in vec3 pos; in uint target_id; diff --git a/ui/src/gridview/render/shaders/pixmix.frag b/ui/src/gridview/render/shaders/pixmix.frag index a6964604..92461bb1 100644 --- a/ui/src/gridview/render/shaders/pixmix.frag +++ b/ui/src/gridview/render/shaders/pixmix.frag @@ -1,4 +1,4 @@ -#version 140 +#version 150 /* * Pixel Mixing filter, also called Area Averaging Scale Filter and many other diff --git a/ui/src/gridview/render/shaders/pixmix.vert b/ui/src/gridview/render/shaders/pixmix.vert index a84d4e56..5fc93e82 100644 --- a/ui/src/gridview/render/shaders/pixmix.vert +++ b/ui/src/gridview/render/shaders/pixmix.vert @@ -1,4 +1,4 @@ -#version 140 +#version 150 in vec2 src_coords; in vec2 dest_coords; diff --git a/ui/src/gridview/render/shaders/quadtree.frag b/ui/src/gridview/render/shaders/quadtree.frag index 99f5acd7..3189f3f5 100644 --- a/ui/src/gridview/render/shaders/quadtree.frag +++ b/ui/src/gridview/render/shaders/quadtree.frag @@ -1,6 +1,6 @@ -#version 140 +#version 150 -in vec2 screen_pos; // 0.0 ... 1.0 +in vec2 ndc_xy; // 0.0 ... 1.0 out vec4 color; @@ -9,7 +9,7 @@ uniform usampler2D quadtree_texture; uniform int layer_count; uniform uint root_idx; -uniform ivec2 quadtree_offset; +uniform ivec2 quadtree_base; uint texture_width = uint(textureSize(quadtree_texture, 0).x); int quadtree_len = 1 << layer_count; @@ -32,7 +32,7 @@ uint getNodeChild(uint node, bvec2 which_child) { } void main() { - ivec2 cell_pos = ivec2(floor(gl_FragCoord.xy)) + quadtree_offset; + ivec2 cell_pos = ivec2(floor(gl_FragCoord.xy)) - quadtree_base; if (cell_pos.x < 0 || cell_pos.x > quadtree_len) discard; if (cell_pos.y < 0 || cell_pos.y > quadtree_len) discard; diff --git a/ui/src/gridview/render/shaders/rgba_2d.frag b/ui/src/gridview/render/shaders/rgba_2d.frag index 553ab0d2..0f6d3671 100644 --- a/ui/src/gridview/render/shaders/rgba_2d.frag +++ b/ui/src/gridview/render/shaders/rgba_2d.frag @@ -1,4 +1,4 @@ -#version 140 +#version 150 in vec4 vColor; diff --git a/ui/src/gridview/render/shaders/rgba_2d.vert b/ui/src/gridview/render/shaders/rgba_2d.vert index b1a1973c..cdc2e013 100644 --- a/ui/src/gridview/render/shaders/rgba_2d.vert +++ b/ui/src/gridview/render/shaders/rgba_2d.vert @@ -1,6 +1,6 @@ -#version 140 +#version 150 -in vec3 pos; +in vec2 pos; in vec4 color; out vec4 vColor; @@ -8,6 +8,6 @@ out vec4 vColor; uniform mat4 matrix; void main() { - gl_Position = matrix * vec4(pos, 1.0); + gl_Position = matrix * vec4(pos, 0.0, 1.0); vColor = color; } diff --git a/ui/src/gridview/render/shaders/rgba_3d.frag b/ui/src/gridview/render/shaders/rgba_3d.frag index dafb4163..65f28649 100644 --- a/ui/src/gridview/render/shaders/rgba_3d.frag +++ b/ui/src/gridview/render/shaders/rgba_3d.frag @@ -1,4 +1,4 @@ -#version 140 +#version 150 in vec3 vPos; in vec4 vColor; diff --git a/ui/src/gridview/render/shaders/rgba_3d.vert b/ui/src/gridview/render/shaders/rgba_3d.vert index 2a7e1f9d..11e9ad63 100644 --- a/ui/src/gridview/render/shaders/rgba_3d.vert +++ b/ui/src/gridview/render/shaders/rgba_3d.vert @@ -1,4 +1,4 @@ -#version 140 +#version 150 in vec3 pos; in vec3 normal; @@ -9,11 +9,19 @@ out vec4 vColor; uniform mat4 matrix; +const float EPSILON = 1.0 / 256.0; + // This line includes another file in this GLSL program. See `shaders/mod.rs`. //#include util/lighting.frag void main() { gl_Position = matrix * vec4(pos, 1.0); + if (color.a < 1.0) { + // Avoid Z-fighting when something transparent is drawn in the same + // position as something opaque; let the transparent thing be in front. + gl_Position.z -= EPSILON; + } + vPos = pos; vColor = color; vColor.rgb *= compute_lighting(normal); diff --git a/ui/src/gridview/render/shaders/screen_pos.vert b/ui/src/gridview/render/shaders/screen_pos.vert deleted file mode 100644 index 9642f171..00000000 --- a/ui/src/gridview/render/shaders/screen_pos.vert +++ /dev/null @@ -1,10 +0,0 @@ -#version 140 - -in vec2 pos; - -out vec2 screen_pos; - -void main() { - gl_Position = vec4(pos, 0.0, 1.0); - screen_pos = pos; -} diff --git a/ui/src/gridview/render/shaders/util/fog.frag b/ui/src/gridview/render/shaders/util/fog.frag index af87c078..61fa3f13 100644 --- a/ui/src/gridview/render/shaders/util/fog.frag +++ b/ui/src/gridview/render/shaders/util/fog.frag @@ -1,10 +1,12 @@ // This file is included in shader programs using an awful hack. // See `shaders/mod.rs`. -uniform vec4 fog_color; -uniform vec3 fog_center; // center of fog sphere -uniform float fog_start; // radius at which fog starts -uniform float fog_end; // radius at which fog reaches maximum +layout(std140) uniform FogParams { + vec3 fog_color; + float fog_start; // radius at which fog starts + vec3 fog_center; // center of fog sphere + float fog_end; // radius at which fog reaches maximum +}; vec4 foggify_color(vec3 pos, vec4 unfogged_color) { float dist = distance(pos, fog_center); diff --git a/ui/src/gridview/render/shaders/util/lighting.frag b/ui/src/gridview/render/shaders/util/lighting.frag index 9e781317..36e2fb1e 100644 --- a/ui/src/gridview/render/shaders/util/lighting.frag +++ b/ui/src/gridview/render/shaders/util/lighting.frag @@ -1,15 +1,17 @@ // This file is included in shader programs using an awful hack. // See `shaders/mod.rs`. -uniform vec3 light_direction; // normalized vector -uniform float light_ambientness; // 0.0 ... 1.0 -uniform float max_light; // 0.0 ... 1.0 +layout(std140) uniform LightingParams { + vec3 light_direction; // normalized vector + float light_ambientness; // 0.0 ... 1.0 + float light_multiplier; // 0.0 ... 1.0 +}; float light_scalar_factor = light_ambientness; float light_vector_factor = 1.0 - light_ambientness; float compute_lighting(vec3 normal) { float light_vector_alignment = dot(normal, normalize(light_direction)) / 2.0 + 0.5; - float color_multiplier = max_light * (light_scalar_factor + light_vector_factor * light_vector_alignment); + float color_multiplier = light_multiplier * (light_scalar_factor + light_vector_factor * light_vector_alignment); return color_multiplier; } diff --git a/ui/src/gridview/render/shaders/wrapped.rs b/ui/src/gridview/render/shaders/wrapped.rs index f5726419..2901cadd 100644 --- a/ui/src/gridview/render/shaders/wrapped.rs +++ b/ui/src/gridview/render/shaders/wrapped.rs @@ -4,6 +4,12 @@ use log::error; use parking_lot::{MappedMutexGuard, Mutex, MutexGuard}; pub struct StaticWrappedShader(pub Program); +impl PartialEq for StaticWrappedShader { + fn eq(&self, other: &Self) -> bool { + std::ptr::eq(self, other) + } +} +impl Eq for StaticWrappedShader {} impl StaticWrappedShader { pub fn load(&self) -> &Program { &self.0 @@ -18,6 +24,12 @@ pub struct DynamicWrappedShader { pub srgb: bool, pub program: Mutex>, } +impl PartialEq for DynamicWrappedShader { + fn eq(&self, other: &Self) -> bool { + std::ptr::eq(self, other) + } +} +impl Eq for DynamicWrappedShader {} impl DynamicWrappedShader { fn _load(&self) -> Option { let vert_contents = std::fs::read_to_string(self.vert_filename) diff --git a/ui/src/gridview/render/vbos.rs b/ui/src/gridview/render/vbos.rs index d040c976..7d3f667a 100644 --- a/ui/src/gridview/render/vbos.rs +++ b/ui/src/gridview/render/vbos.rs @@ -51,13 +51,13 @@ impl VboCache { } pub fn gridlines_quad(&mut self, rect: FRect2D) -> &mut VertexBuffer { - let [x0, y0] = rect.min().to_f32_array(); - let [x1, y1] = rect.max().to_f32_array(); + let [x1, y1] = rect.min().to_f32_array(); + let [x2, y2] = rect.max().to_f32_array(); self.gridlines_quad.write(&[ - PosVertex2D { pos: [x0, y0] }, - PosVertex2D { pos: [x1, y0] }, - PosVertex2D { pos: [x0, y1] }, PosVertex2D { pos: [x1, y1] }, + PosVertex2D { pos: [x2, y1] }, + PosVertex2D { pos: [x1, y2] }, + PosVertex2D { pos: [x2, y2] }, ]); &mut self.gridlines_quad } diff --git a/ui/src/gridview/render/vertices.rs b/ui/src/gridview/render/vertices.rs index dc0fcfb7..fd13491b 100644 --- a/ui/src/gridview/render/vertices.rs +++ b/ui/src/gridview/render/vertices.rs @@ -1,6 +1,7 @@ //! OpenGL vertex types. use glium::implement_vertex; +use palette::{Pixel, Srgba}; /// Vertex containing only a 2D NDC position. #[derive(Debug, Default, Copy, Clone)] @@ -17,39 +18,34 @@ pub struct TexturePosVertex { } implement_vertex!(TexturePosVertex, src_coords, dest_coords); -/// Vertex containing a 3D floating-point position and an RGBA color. +/// Vertex containing a 3D floating-point position and an sRGBA color. #[derive(Debug, Default, Copy, Clone)] pub struct Vertex2D { - pub pos: [f32; 3], - pub color: [f32; 4], -} -implement_vertex!(Vertex2D, pos, color); -impl From<([isize; 2], f32, [f32; 4])> for Vertex2D { - fn from(pos_and_color: ([isize; 2], f32, [f32; 4])) -> Self { - let ([x, y], z, color) = pos_and_color; - Self::from(([x as f32, y as f32, z], color)) - } + pos: [f32; 2], + color: [u8; 4], } -impl From<([f32; 3], [f32; 4])> for Vertex2D { - fn from(pos_and_color: ([f32; 3], [f32; 4])) -> Self { - let (pos, color) = pos_and_color; +implement_vertex!(Vertex2D, pos normalize(false), color normalize(true)); +impl Vertex2D { + pub fn new(pos: [f32; 2], color: Srgba) -> Self { + let color = color.into_format().into_raw(); Self { pos, color } } } -/// Vertex containing a 3D position, normal vector, and an RGBA color. +/// Vertex containing a 3D position, normal vector, and an sRGBA color. #[derive(Debug, Default, Copy, Clone)] pub struct Vertex3D { - pub pos: [f32; 3], - pub normal: [i8; 3], - pub color: [u8; 4], + pos: [f32; 3], + normal: [i8; 3], + color: [u8; 4], +} +implement_vertex!(Vertex3D, pos normalize(false), normal normalize(false), color normalize(true)); +impl Vertex3D { + pub fn new(pos: [f32; 3], normal: [i8; 3], color: Srgba) -> Self { + let color = color.into_format().into_raw(); + Self { pos, normal, color } + } } -implement_vertex!( - Vertex3D, - pos normalize(false), - normal normalize(true), - color normalize(true) -); /// Vertex containing a 3D floating-point position and an ID, for use with the /// pixel buffer object to map out regions that the mouse cursor can interact diff --git a/ui/src/gridview/screenpos.rs b/ui/src/gridview/screenpos.rs new file mode 100644 index 00000000..62e0bfc5 --- /dev/null +++ b/ui/src/gridview/screenpos.rs @@ -0,0 +1,502 @@ +use cgmath::InnerSpace; +use log::warn; +use std::fmt; + +use ndcell_core::prelude::*; + +use super::algorithms::raycast; +use super::generic::GridViewDimension; +use super::viewpoint::{CellTransform3D, Viewpoint3D}; +use crate::commands::{DragCmd, DrawMode}; +use crate::ext::FVecConvertExt; +use crate::mouse::MouseDisplayMode; +use crate::{Face, Plane}; + +pub type OperationPos = <::ScreenPos as ScreenPosTrait>::OperationPos; + +pub trait OperationPosTrait: fmt::Debug + Sized { + type D: Dim; + + fn cell(&self) -> &BigVec; + fn into_cell(self) -> BigVec; +} + +/// Convenient representation of a pixel position on the screen. +pub trait ScreenPosTrait: Sized { + type D: Dim; + type DrawState: 'static + fmt::Debug; + type OperationPos: OperationPosTrait; + + /// Returns the original pixel location. + fn pixel(&self) -> FVec2D; + /// Returns the render cell layer. + fn layer(&self) -> Layer; + + /// Returns the cell to highlight. + fn op_pos_for_mouse_display_mode( + &self, + mouse_display_mode: MouseDisplayMode, + ) -> Option { + match mouse_display_mode { + MouseDisplayMode::Draw(draw_mode) => { + self.op_pos_for_drag_command(&DragCmd::DrawFreeform(draw_mode)) + } + + MouseDisplayMode::Select => self.op_pos_for_drag_command(&DragCmd::SelectNewRect), + MouseDisplayMode::ResizeSelectionToCursor => { + self.op_pos_for_drag_command(&DragCmd::ResizeSelectionToCursor) + } + + _ => None, + } + } + /// Returns the cell that a drag command operates on initially. + fn op_pos_for_drag_command(&self, command: &DragCmd) -> Option { + self.op_pos_for_continue_drag_command(command, self) + } + /// Returns the cell that a drag command operates on, given the starting + /// position of the drag. + fn op_pos_for_continue_drag_command( + &self, + command: &DragCmd, + start: &Self, + ) -> Option { + match command { + DragCmd::DrawFreeform(draw_mode) => self.pos_to_draw(*draw_mode, start), + + DragCmd::SelectNewRect | DragCmd::ResizeSelectionToCursor => self.pos_to_select(start), + + _ => None, + } + } + + /// Returns the cell that a draw drag command operates on. + fn pos_to_draw(&self, draw_mode: DrawMode, start: &Self) -> Option; + /// Returns the cell that a selection drag command operates on. + fn pos_to_select(&self, start: &Self) -> Option; + + /// Returns the position that most view commands operate on. + fn scale_invariant_pos(&self) -> Option>; + + /// Returns the delta between `initial` and `self` when resizing + /// `initial_rect` along `resize_vector`. + fn rect_resize_delta( + &self, + initial_rect: &FixedRect, + initial_screenpos: &Self, + resize_vector: &IVec, + ) -> Option>; + /// Returns the delta between `initial` and `self` along `parallel_face` of + /// `initial_rect`. `parallel_face` is ignored in 2D but must be `Some` in + /// 3D. + fn rect_move_delta( + &self, + initial_rect: &FixedRect, + initial_screenpos: &Self, + parallel_face: Option, + ) -> Option>; + /// Returns the initial position for absolute resizing of the selection. + fn absolute_selection_resize_start_pos(&self) -> Option>; +} + +#[derive(Debug, Clone)] +pub struct ScreenPos2D { + pub(super) pixel: FVec2D, + pub(super) layer: Layer, + + /// Global cell position. + pub pos: FixedVec2D, +} +impl ScreenPosTrait for ScreenPos2D { + type D = Dim2D; + type DrawState = (); + type OperationPos = OperationPos2D; + + fn pixel(&self) -> FVec2D { + self.pixel + } + fn layer(&self) -> Layer { + self.layer + } + + fn pos_to_draw(&self, _draw_mode: DrawMode, _start: &Self) -> Option { + if self.layer != Layer(0) { + return None; + } + + Some(OperationPos2D { cell: self.cell() }) + } + fn pos_to_select(&self, _start: &Self) -> Option { + Some(OperationPos2D { cell: self.cell() }) + } + + fn scale_invariant_pos(&self) -> Option { + Some(self.pos()) + } + + fn rect_resize_delta( + &self, + _initial_rect: &FixedRect2D, + initial_screenpos: &Self, + _resize_vector: &IVec2D, + ) -> Option { + Some(self.pos() - &initial_screenpos.pos) + } + fn rect_move_delta( + &self, + _initial_rect: &FixedRect2D, + initial_screenpos: &Self, + _parallel_face: Option, + ) -> Option { + Some(self.pos() - &initial_screenpos.pos) + } + fn absolute_selection_resize_start_pos(&self) -> Option { + Some(self.pos()) + } +} + +impl ScreenPos2D { + /// Returns the global cell position of the mouse. + pub fn pos(&self) -> FixedVec2D { + self.pos.clone() + } + /// Returns the global cell coordinates at the pixel. + pub fn cell(&self) -> BigVec2D { + self.pos.floor() + } + /// Returns the global cell rectangle of cells inside the pixel. + pub fn rect(&self) -> BigRect2D { + let render_cell_len = self.layer.big_len(); + // Round to render cell. + let base_pos = self.cell().div_floor(&render_cell_len) * render_cell_len; + self.layer.big_rect() + base_pos + } +} + +#[derive(Debug, Clone)] +pub struct OperationPos2D { + pub cell: BigVec2D, +} +impl OperationPosTrait for OperationPos2D { + type D = Dim2D; + + fn cell(&self) -> &BigVec2D { + &self.cell + } + fn into_cell(self) -> BigVec2D { + self.cell + } +} + +#[derive(Debug, Clone)] +pub struct ScreenPos3D { + pub(super) pixel: FVec2D, + pub(super) layer: Layer, + pub(super) xform: CellTransform3D, + + /// Nearest raycast hit. + pub raycast: Option, + /// Ray intersection with nearest cell. + pub raycast_octree_hit: Option, + /// Ray intersection with gridline plane. + pub raycast_gridlines_hit: Option, + /// Ray intersection with selection boundary. + pub raycast_selection_hit: Option, +} +impl ScreenPosTrait for ScreenPos3D { + type D = Dim3D; + type DrawState = (BigVec3D, Plane); + type OperationPos = OperationPos3D; + + fn pixel(&self) -> FVec2D { + self.pixel + } + fn layer(&self) -> Layer { + self.layer + } + + fn pos_to_draw(&self, draw_mode: DrawMode, initial: &Self) -> Option { + if self.layer != Layer(0) { + return None; + } + + // Determine the plane to draw in. + let initial_raycast = initial.raycast.as_ref()?; + let draw_plane = match draw_mode { + DrawMode::Place => initial_raycast.outside_plane_face(), + DrawMode::Replace => initial_raycast.inside_plane_face(), + DrawMode::Erase => initial_raycast.inside_plane_face(), + }; + + // Raycast from the cursor to that plane. + let cell_to_draw = self.raycast_to_plane_face(&draw_plane)?; + + Some(OperationPos3D { + cell: cell_to_draw, + face: draw_plane.face, + }) + } + fn pos_to_select(&self, start: &Self) -> Option { + // First, do a normal raycast. If we hit something, use that. + if let Some(hit) = &self.raycast { + return Some(OperationPos3D { + cell: hit.inside_cell(), + face: hit.inside_plane_face().face, + }); + } + + // Determine the plane to select in. + let initial_raycast = start.raycast.as_ref()?; + let select_plane = initial_raycast.inside_plane_face(); + // Raycast from the cursor to that plane. + let cell_to_select = self.raycast_to_plane_face(&select_plane)?; + + Some(OperationPos3D { + cell: cell_to_select, + face: select_plane.face, + }) + } + + fn scale_invariant_pos(&self) -> Option> { + None + } + + fn rect_resize_delta( + &self, + initial_rect: &FixedRect, + initial_screenpos: &Self, + resize_vector: &IVec, + ) -> Option> { + // Assume `resize_vector` is a unit vector along one axis. If it isn't, + // throw a warning and move on. + let axis = resize_vector.abs().max_axis(); + let face = if resize_vector[axis].is_positive() { + Face::positive(axis) + } else { + Face::negative(axis) + }; + if face.normal_ivec() != *resize_vector { + warn!( + "rect_resize_delta() received weird resize_vector {}; assuming it is {}", + resize_vector, face, + ); + } + + let selection_face_plane = face.plane_of(initial_rect); + let line_start = initial_screenpos.raycast_to_plane(&selection_face_plane)?; + let line_delta = face.normal_fvec(); + let line_end = self.nearest_global_pos_on_line(&line_start, line_delta)?; + + Some(line_end - line_start) + } + fn rect_move_delta( + &self, + initial_rect: &FixedRect, + initial_screenpos: &Self, + parallel_face: Option, + ) -> Option> { + if parallel_face.is_none() { + warn!("ScreenPos3D::rect_move_delta() received `parallel_face = None`"); + } + + let selection_face_plane = parallel_face?.plane_of(initial_rect); + let start_pos = initial_screenpos.raycast_to_plane(&selection_face_plane)?; + let end_pos = self.raycast_to_plane(&selection_face_plane)?; + + Some(end_pos - start_pos) + } + fn absolute_selection_resize_start_pos(&self) -> Option> { + self.raycast.as_ref().map(|hit| hit.pos.clone()) + } +} +impl ScreenPos3D { + /// Returns the global cell position at the mouse cursor in the plane parallel + /// to the screen at the distance of the viewpoint pivot. + pub fn global_pos_at_pivot_depth(&self) -> FixedVec3D { + self.xform + .pixel_to_global_pos(self.pixel, Viewpoint3D::DISTANCE_TO_PIVOT) + } + + fn raycast_to_plane_face(&self, plane: &PlaneFace) -> Option { + let mut ret = self.raycast_to_plane(&plane.fixedpoint_plane())?.floor(); + + // Account for possible off-by-one error during the raycast. + ret[plane.face.normal_axis()] = plane.coord.clone(); + + Some(ret) + } + pub fn raycast_to_plane(&self, plane: &Plane) -> Option { + self.xform.pixel_to_global_pos_in_plane(self.pixel, plane) + } + + pub fn nearest_global_pos_on_line( + &self, + line_start: &FixedVec3D, + line_delta: FVec3D, + ) -> Option { + let line_start = self.xform.global_to_local_float(line_start)?; + let local_result = self.nearest_local_pos_on_line(line_start, line_delta)?; + Some(self.xform.local_to_global_float(local_result)) + } + pub fn nearest_local_pos_on_line( + &self, + line_start: FVec3D, + line_delta: FVec3D, + ) -> Option { + // https://math.stackexchange.com/questions/1414285/ + + let p1 = line_start.to_cgmath_point3(); + let d1 = line_delta.to_cgmath_vec3(); + let (pixel_ray_start, pixel_ray_delta) = self.xform.pixel_to_local_ray(self.pixel); + let p2 = pixel_ray_start.to_cgmath_point3(); + let d2 = pixel_ray_delta.to_cgmath_vec3(); + + let n = d1.cross(d2); + let n1 = d1.cross(n); + let n2 = d2.cross(n); + + let t2 = (p1 - p2).dot(n1) / d2.dot(n1); + if t2.is_finite() && t2 > 0.0 { + let t1 = (p2 - p1).dot(n2) / d1.dot(n2); + let c1 = p1 + t1 * d1; + Some(NdVec([ + r64(c1.x as f64), + r64(c1.y as f64), + r64(c1.z as f64), + ])) + } else { + None + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RaycastHit { + /// Point of intersection between ray and cell. + pub pos: FixedVec3D, + /// Position of intersected cell. + pub cell: BigVec3D, + /// Face of cell intersected. + pub face: Face, + /// Type of thing hit. + pub thing: RaycastHitThing, + /// Abstract distance to intersection. + distance: R64, +} +impl PartialOrd for RaycastHit { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Ord for RaycastHit { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + if self.cell == other.cell && self.face == other.face { + // Z-fighting! Sort by thing instead. + self.thing.cmp(&other.thing) + } else { + self.distance.cmp(&other.distance) + } + } +} +impl RaycastHit { + pub fn new(xform: &CellTransform3D, hit: raycast::Hit, thing: RaycastHitThing) -> Self { + Self { + pos: xform.local_to_global_float(hit.pos_float), + cell: xform.local_to_global_int(hit.pos_int), + face: hit.face, + thing, + distance: hit.t0, + } + } + + /// Returns the cell hit by the ray. If the ray hits the gridline plane, + /// then the cell in front of the gridlines is considered hit. + pub fn inside_cell(&self) -> BigVec3D { + match self.thing { + RaycastHitThing::Gridlines => self.outside_cell(), + RaycastHitThing::Cell => self.cell.clone(), + } + } + /// Returns the cell in front of the one hit by the ray. + pub fn outside_cell(&self) -> BigVec3D { + self.face.normal_bigvec() + &self.cell + } + + /// Returns the face of the cell hit by the ray, facing toward the camera. + /// If the ray hits the gridline plane, then the cell in front of the + /// gridlines is considered hit, and the face will be opposite. + pub fn inside_face(&self) -> Face { + match self.thing { + RaycastHitThing::Gridlines => self.outside_face(), + RaycastHitThing::Cell => self.face, + } + } + /// Returns the face of the cell in front of the one hit by the ray, facing + /// away from the camera. This face has a normal vector in the same + /// direction as the ray. + pub fn outside_face(&self) -> Face { + self.face.opposite() + } + + /// Returns the plane face of the cell hit by the ray, facing toward the + /// camera. If the ray hits the gridline plane, then the result is the same + /// as `outside_plane_face()`. + fn inside_plane_face(&self) -> PlaneFace { + match self.thing { + RaycastHitThing::Gridlines => self.outside_plane_face(), + RaycastHitThing::Cell => { + let face = self.inside_face(); + let coord = self.inside_cell()[face.normal_axis()].clone(); + PlaneFace { face, coord } + } + } + } + /// Returns the plane face of the cell in front of the one hit by the ray, + /// facing toward the cell hit. + fn outside_plane_face(&self) -> PlaneFace { + let face = self.outside_face(); + let coord = self.outside_cell()[face.normal_axis()].clone(); + PlaneFace { face, coord } + } +} + +#[derive(Debug, Clone)] +pub struct OperationPos3D { + pub cell: BigVec3D, + pub face: Face, +} +impl OperationPosTrait for OperationPos3D { + type D = Dim3D; + + fn cell(&self) -> &BigVec3D { + &self.cell + } + fn into_cell(self) -> BigVec3D { + self.cell + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum RaycastHitThing { + Gridlines, // gridlines in front + Cell, // then cells +} + +/// A global cell plane and one of its two faces. +#[derive(Debug, Clone, PartialEq, Eq)] +struct PlaneFace { + pub face: Face, + pub coord: BigInt, +} +impl PlaneFace { + fn fixedpoint_plane(&self) -> Plane { + Plane { + axis: self.face.normal_axis(), + coordinate: if self.face.sign() == Sign::Plus { + FixedPoint::from(&self.coord + 1) + } else { + FixedPoint::from(self.coord.clone()) + }, + } + } +} diff --git a/ui/src/gridview/selection.rs b/ui/src/gridview/selection.rs index ec3ac9a3..e7a50276 100644 --- a/ui/src/gridview/selection.rs +++ b/ui/src/gridview/selection.rs @@ -1,6 +1,6 @@ use ndcell_core::prelude::*; -use crate::CONFIG; +use crate::{Face, CONFIG}; pub type Selection2D = Selection; pub type Selection3D = Selection; @@ -75,32 +75,44 @@ impl Selection { } } -/// Resizes a selection given cursor movement from `drag_start_pos` to -/// `drag_end_pos` based on the distance covered during the drag. +/// Resizes a selection along a global `resize_delta`. +/// +/// `resize_vector` is a vector where the sign of each component indicates the +/// direction to resize along that axis, with zero for axes where the selection +/// rectangle is not resized. pub fn resize_selection_relative( initial_selection: &BigRect, - drag_start_pos: &FixedVec, - drag_end_pos: &FixedVec, - axes: AxisSet, + resize_delta: &FixedVec, + resize_vector: &IVec, ) -> BigRect { - // Farthest corner stays fixed. - let pos1 = initial_selection.farthest_corner(drag_start_pos); - // Closest corner varies. - let mut pos2 = initial_selection.closest_corner(drag_start_pos); + // `pos1` stays fixed; `pos2` varies. + let mut pos1 = initial_selection.min(); + let mut pos2 = initial_selection.max(); + for &ax in D::axes() { + if resize_vector[ax] < 0 { + std::mem::swap(&mut pos1[ax], &mut pos2[ax]); + } + } + + for &ax in D::axes() { + if resize_vector[ax] != 0 { + pos2[ax] += resize_delta[ax].round(); - // Use delta from original cursor position to new cursor - // position. - let delta = drag_end_pos - drag_start_pos; - for axis in axes { - pos2[axis] += delta[axis].round(); + // Clamp to `pos1`; prevent the selection from turning "inside out." + if resize_vector[ax] > 0 && pos1[ax] > pos2[ax] + || resize_vector[ax] < 0 && pos1[ax] < pos2[ax] + { + pos2[ax] = pos1[ax].clone() + } + } } NdRect::span(pos1, pos2) } /// Resizes a selection given cursor movement from `drag_start_pos` to -/// `drag_end_pos`, using the absolute `drag_start_pos` to infer which axes to -/// modify. +/// `drag_end_render_cell`, using the absolute `drag_start_pos` to infer which +/// axes to modify. pub fn resize_selection_absolute( initial_selection: &BigRect, drag_start_pos: &FixedVec, @@ -115,16 +127,54 @@ pub fn resize_selection_absolute( let drag_end_min = drag_end_render_cell.min(); let drag_end_max = drag_end_render_cell.max(); for ax in axes { - pos2[ax] = if drag_end_max[ax] > pos1[ax] { - drag_end_max[ax].clone() + pos2[ax] = if pos2[ax] > pos1[ax] { + std::cmp::max(drag_end_max[ax].clone(), pos1[ax].clone()) } else { - drag_end_min[ax].clone() + std::cmp::min(drag_end_min[ax].clone(), pos1[ax].clone()) }; } NdRect::span(pos1, pos2) } +/// Resizes a selection to `face` of `drag_end_render_cell` along the axis +/// normal to `face`. +pub fn resize_selection_to_face( + initial_selection: &BigRect, + drag_start_pos: &FixedVec, + drag_end_render_cell: &BigRect, + face: Face, +) -> BigRect { + // Farthest corner stays fixed. + let pos1 = initial_selection.farthest_corner(drag_start_pos); + // Closest corner varies. + let mut pos2 = initial_selection.closest_corner(drag_start_pos); + + let drag_end_min = drag_end_render_cell.min(); + let drag_end_max = drag_end_render_cell.max(); + + let axis = face.normal_axis(); + pos2[axis] = match face.sign() { + Sign::Minus => { + if pos2[axis] > pos1[axis] { + std::cmp::max(drag_end_min[axis].clone() - 1, pos1[axis].clone()) + } else { + std::cmp::min(drag_end_min[axis].clone(), pos1[axis].clone()) + } + } + Sign::NoSign => unreachable!(), + Sign::Plus => { + if pos2[axis] > pos1[axis] { + std::cmp::max(drag_end_max[axis].clone(), pos1[axis].clone()) + } else { + std::cmp::min(drag_end_max[axis].clone() + 1, pos1[axis].clone()) + } + } + }; + + NdRect::span(pos1, pos2) +} + /// Returns a set of axes along which to resize a selection, given the initial /// cursor position. pub fn absolute_selection_resize_axes( diff --git a/ui/src/gridview/view2d.rs b/ui/src/gridview/view2d.rs index 73eb0916..1b6ccb60 100644 --- a/ui/src/gridview/view2d.rs +++ b/ui/src/gridview/view2d.rs @@ -1,243 +1,39 @@ -use anyhow::{Context, Result}; -use log::warn; +use anyhow::Result; use ndcell_core::prelude::*; +use super::drag::Drag; use super::generic::{GenericGridView, GridViewDimension}; -use super::history::History; use super::render::{CellDrawParams, GridViewRender2D, RenderParams, RenderResult}; -use super::selection::Selection2D; +use super::screenpos::{ScreenPos2D, ScreenPosTrait}; use super::viewpoint::{Viewpoint, Viewpoint2D}; -use super::{DragHandler, DragOutcome, DragType}; -use crate::commands::*; -use crate::mouse::MouseDisplay; -use crate::{Scale, CONFIG}; +use crate::mouse::MouseDisplayMode; -pub type GridView2D = GenericGridView; +pub type GridView2D = GenericGridView; -#[derive(Debug, Default)] -pub struct GridViewDim2D; -impl GridViewDimension for GridViewDim2D { - type D = Dim2D; +impl GridViewDimension for Dim2D { type Viewpoint = Viewpoint2D; + type ScreenPos = ScreenPos2D; - fn do_view_command(this: &mut GridView2D, command: ViewCommand) -> Result<()> { - // Handle `FitView` specially because it depends on the cell contents of - // the automaton. - if matches!(command, ViewCommand::FitView) { - if let Some(pattern_bounding_rect) = this.automaton.ndtree.bounding_rect() { - // Set position. - let NdVec([x, y]) = pattern_bounding_rect.center(); - this.do_command(ViewCommand::GoTo2D { - x: Some(x.into()), - y: Some(y.into()), - relative: false, - scaled: false, - })?; - - // Set scale. - let pattern_size = pattern_bounding_rect.size(); - let target_size = this.viewpoint().target_dimensions(); - let scale = Scale::from_fit(pattern_size, target_size); - this.do_command(ViewCommand::GoToScale(scale.floor()))?; - } - return Ok(()); - } - - // Delegate to the viewpoint. - let maybe_new_drag_handler = this - .viewpoint_interpolator - .do_view_command(command) - .context("Executing view command")?; - - // Update drag handler, if the viewpoint gave one. - if !this.is_dragging() { - if let Some(mut interpolator_drag_handler) = maybe_new_drag_handler { - this.start_drag( - DragType::MovingView, - Box::new(move |gridview: &mut GridView2D, cursor_pos| { - interpolator_drag_handler(&mut gridview.viewpoint_interpolator, cursor_pos) - }) as DragHandler, - ); - } - } - - Ok(()) - } - fn do_draw_command(this: &mut GridView2D, command: DrawCommand) -> Result<()> { - match command { - DrawCommand::SetState(new_selected_cell_state) => { - this.selected_cell_state = new_selected_cell_state; - } - DrawCommand::Drag(c, cursor_start) => { - let initial_cell = match this.screen_pos(cursor_start).draw_cell() { - Some(cell_pos) => cell_pos, - None => return Ok(()), // Don't draw if the scale is too small. - }; - - let new_cell_state = c.mode.cell_state( - this.automaton.ndtree.get_cell(&initial_cell), - this.selected_cell_state, - ); + type Data = (); - let new_drag_handler: DragHandler = match c.shape { - DrawShape::Freeform => { - make_freeform_draw_drag_handler(initial_cell, new_cell_state) - } - DrawShape::Line => { - // TODO: implement drawing straight line - warn!("Line drawing is not yet implemented!"); - return Ok(()); - } - }; - - this.reset_worker_thread(); - this.record(); - this.start_drag(DragType::Drawing, new_drag_handler); - } - DrawCommand::Confirm => { - if this.is_drawing() { - this.stop_drag(); - } - } - DrawCommand::Cancel => { - if this.is_drawing() { - this.stop_drag(); - this.undo(); - } - } - } - Ok(()) + fn focus(this: &mut GridView2D, pos: &ScreenPos2D) { + this.target_viewpoint().set_center(pos.pos()); } - fn do_select_command(this: &mut GridView2D, command: SelectCommand) -> Result<()> { - match command { - SelectCommand::Drag(c, cursor_start) => { - let initial_pos = this.screen_pos(cursor_start); - - let new_drag_handler = match c { - SelectDragCommand::NewRect => make_new_rect_selection_drag_handler(initial_pos), - SelectDragCommand::Resize { axes, .. } => { - make_resize_selection_drag_handler(initial_pos, axes) - } - SelectDragCommand::ResizeToCell => { - make_resize_selection_to_cell_drag_handler(initial_pos) - } - SelectDragCommand::MoveSelection => { - make_move_selection_drag_handler(initial_pos) - } - SelectDragCommand::MoveCells => { - make_move_or_copy_selected_cells_drag_handler(initial_pos, false) - } - SelectDragCommand::CopyCells => { - make_move_or_copy_selected_cells_drag_handler(initial_pos, true) - } - }; - - let wait_for_drag_threshold = match c { - SelectDragCommand::NewRect => true, - SelectDragCommand::Resize { .. } => true, - SelectDragCommand::ResizeToCell => false, - SelectDragCommand::MoveSelection => true, - SelectDragCommand::MoveCells => true, - SelectDragCommand::CopyCells => true, - }; - - let new_drag_handler_with_threshold: DragHandler = - make_selection_drag_handler_with_threshold( - cursor_start, - wait_for_drag_threshold, - new_drag_handler, - ); - - match c { - SelectDragCommand::NewRect - | SelectDragCommand::Resize { .. } - | SelectDragCommand::ResizeToCell - | SelectDragCommand::MoveSelection => { - if CONFIG.lock().hist.record_select { - this.reset_worker_thread(); - this.record(); - } - } - SelectDragCommand::MoveCells | SelectDragCommand::CopyCells => { - this.reset_worker_thread(); - this.record(); - } - } - - if let SelectDragCommand::NewRect = c { - // Deselect immediately; don't wait for drag threshold. - this.deselect(); - } - - this.start_drag(DragType::Selecting, new_drag_handler_with_threshold); - } - - SelectCommand::SelectAll => { - this.deselect(); // take into account pasted cells - this.set_selection(this.automaton.ndtree.bounding_rect().map(Selection2D::from)) - } - SelectCommand::Deselect => { - this.deselect(); - } - - SelectCommand::Copy(format) => { - if this.selection.is_some() { - let result = this.export(format); - match result { - Ok(s) => crate::clipboard_compat::clipboard_set(s)?, - Err(msg) => warn!("Failed to generate {}: {}", format, msg), - } - } - } - SelectCommand::Paste => { - let old_sel_rect = this.selection_rect(); - - this.record(); - let string_from_clipboard = crate::clipboard_compat::clipboard_get()?; - let result = - Selection2D::from_str(&string_from_clipboard, this.automaton.ndtree.pool()); - match result { - Ok(sel) => { - this.set_selection(sel); - this.ensure_selection_visible(); - - // If selection size is the same, preserve position. - if let Some((old_rect, new_sel)) = old_sel_rect.zip(this.selection.as_mut()) - { - if old_rect.size() == new_sel.rect.size() { - *new_sel = new_sel.move_by(old_rect.min() - new_sel.rect.min()); - } - } - } - Err(errors) => warn!("Failed to load pattern: {:?}", errors), - } - } - SelectCommand::Delete => { - if this.selection.is_some() { - this.record(); - this.selection.as_mut().unwrap().cells = None; - this.grab_selected_cells(); - this.selection.as_mut().unwrap().cells = None; - } - } - - SelectCommand::Cancel => { - if let Some(sel) = &this.selection { - if sel.cells.is_some() { - this.drop_selected_cells(); - } else { - this.deselect(); - } - } - } - } - Ok(()) + fn resize_selection_to_cursor( + rect: &BigRect2D, + start: &FixedVec2D, + end: &BigRect2D, + _drag: &Drag, + ) -> BigRect2D { + super::selection::resize_selection_absolute(&rect, &start, &end) } fn render(this: &mut GridView2D, params: RenderParams<'_>) -> Result { let mouse = params.mouse; - let mouse_pos = mouse.pos.map(|pixel| this.screen_pos(pixel)); + let screen_pos = mouse.pos.map(|pixel| this.screen_pos(pixel)); + let pos_to_highlight = this.cell_to_highlight(&mouse); + let rect_cell_to_highlight = this.render_cell_rect_to_highlight(&mouse); let mut frame = GridViewRender2D::new(params, this.viewpoint()); @@ -246,6 +42,7 @@ impl GridViewDimension for GridViewDim2D { ndtree: &this.automaton.ndtree, alpha: 1.0, rect: None, + interactive: true, })?; // Draw selection cells. if let Some(selection) = &this.selection { @@ -254,261 +51,69 @@ impl GridViewDimension for GridViewDim2D { ndtree, alpha: 1.0, rect: Some(&selection.rect), + interactive: false, })?; } } // Draw gridlines. - frame.draw_gridlines()?; + frame.add_gridlines_overlay(); // Draw mouse display. - if let Some(screen_pos) = &mouse_pos { - match mouse.display { - MouseDisplay::Draw => { - if let Some(cell_pos) = screen_pos.draw_cell() { - frame.draw_hover_highlight(&cell_pos, crate::colors::HOVERED_DRAW)?; - } + if let Some(pos) = &pos_to_highlight { + match mouse.display_mode { + MouseDisplayMode::Draw(draw_mode) => { + frame.add_hover_draw_overlay(&pos.cell, draw_mode); } - MouseDisplay::Select => { - let cell_pos = screen_pos.cell(); - frame.draw_hover_highlight(&cell_pos, crate::colors::HOVERED_SELECT)?; + MouseDisplayMode::Select => { + frame.add_hover_select_overlay(&pos.cell); } _ => (), } } - // Draw selection highlight. - if let Some(selection) = &this.selection { - frame.draw_selection_highlight(selection.rect.clone(), selection.cells.is_none())?; - } - // Draw selection preview after drawing selection. - if mouse.display == MouseDisplay::ResizeSelectionAbsolute && !this.is_dragging() { - if let (Some(mouse_pos), Some(s)) = (mouse_pos, this.selection.as_ref()) { - frame.draw_absolute_selection_resize_preview(s.rect.clone(), &mouse_pos)?; - } - } - - frame.finish() - } -} - -fn make_freeform_draw_drag_handler( - mut pos1: BigVec2D, - new_cell_state: u8, -) -> DragHandler { - Box::new(move |this, new_cursor_pos| { - let pos2 = match this.screen_pos(new_cursor_pos).draw_cell() { - Some(cell_pos) => cell_pos, - None => return Ok(DragOutcome::Cancel), // Don't draw if the scale is too small. - }; - for pos in crate::math::bresenham::line(pos1.clone(), pos2.clone()) { - this.automaton.ndtree.set_cell(&pos, new_cell_state); - } - pos1 = pos2.clone(); - Ok(DragOutcome::Continue) - }) -} - -type SelectionDragHandler = Box Result>; - -fn make_selection_drag_handler_with_threshold( - cursor_start: FVec2D, - wait_for_drag_threshold: bool, - mut inner_drag_handler: SelectionDragHandler, -) -> DragHandler { - let drag_threshold = r64(CONFIG.lock().mouse.drag_threshold); - - // State variable to be moved into the closure and used by the - // drag handler. - let mut past_drag_threshold: bool = false; - if !wait_for_drag_threshold { - past_drag_threshold = true; - } - - Box::new(move |this, new_cursor_pos| { - if !((new_cursor_pos - cursor_start).abs() < FVec::repeat(drag_threshold)) { - past_drag_threshold = true; - } - if past_drag_threshold { - let screen_pos = this.screen_pos(new_cursor_pos); - inner_drag_handler(this, screen_pos) - } else { - Ok(DragOutcome::Continue) - } - }) -} - -fn make_new_rect_selection_drag_handler(initial_pos: ScreenPos2D) -> SelectionDragHandler { - Box::new(move |this, new_pos| { - this.set_selection_rect(Some(NdRect::span_rects(initial_pos.rect(), new_pos.rect()))); - Ok(DragOutcome::Continue) - }) -} -fn make_resize_selection_drag_handler( - initial_pos: ScreenPos2D, - axes: AxisSet, -) -> SelectionDragHandler { - let mut initial_selection = None; - - Box::new(move |this, new_pos| { - initial_selection = initial_selection.take().or_else(|| this.deselect()); - if let Some(s) = &initial_selection { - this.set_selection_rect(Some(super::selection::resize_selection_relative( - &s.rect, - &initial_pos.pos(), - &new_pos.pos(), - axes, - ))); - Ok(DragOutcome::Continue) - } else { - // There is no selection to resize. - Ok(DragOutcome::Cancel) - } - }) -} -fn make_resize_selection_to_cell_drag_handler(initial_pos: ScreenPos2D) -> SelectionDragHandler { - let mut initial_selection = None; - - Box::new(move |this, new_pos| { - initial_selection = initial_selection.take().or_else(|| this.deselect()); - if let Some(s) = &initial_selection { - this.set_selection_rect(Some(super::selection::resize_selection_absolute( - &s.rect, - &initial_pos.pos(), - &new_pos.rect(), - ))); - Ok(DragOutcome::Continue) - } else { - // There is no selection to resize. - Ok(DragOutcome::Cancel) - } - }) -} -fn make_move_selection_drag_handler(initial_pos: ScreenPos2D) -> SelectionDragHandler { - let mut initial_selection = None; - - Box::new(move |this, new_pos| { - initial_selection = initial_selection.take().or_else(|| this.deselect()); - if let Some(s) = &initial_selection { - let delta = (new_pos.pos() - initial_pos.pos()).round(); - this.set_selection_rect(Some(s.rect.clone() + delta)); - Ok(DragOutcome::Continue) - } else { - // There is no selection to move. - Ok(DragOutcome::Cancel) - } - }) -} -fn make_move_or_copy_selected_cells_drag_handler( - initial_pos: ScreenPos2D, - copy: bool, -) -> SelectionDragHandler { - let mut initial_selection = None; - Box::new(move |this, new_pos| { - initial_selection = initial_selection.take().or_else(|| { - if copy { - this.grab_copy_of_selected_cells(); + if let Some(selection) = &this.selection { + // Draw selection highlight. + if selection.cells.is_some() { + frame.add_selection_cells_highlight_overlay(&selection.rect); } else { - this.grab_selected_cells(); - } - this.selection.take() - }); - if let Some(s) = &initial_selection { - let delta = (new_pos.pos() - initial_pos.pos()).round(); - this.selection = Some(s.move_by(delta)); - Ok(DragOutcome::Continue) - } else { - // There is no selection to move. - Ok(DragOutcome::Cancel) - } - }) -} - -impl GridView2D { - /// Moves the selection to the center of the screen along each axis for - /// which it is outside the viewport. - fn ensure_selection_visible(&mut self) { - if let Some(mut sel) = self.selection.take() { - // The number of render cells of padding to ensure. - const PADDING: usize = 2; - - let (render_cell_layer, _) = self.viewpoint().render_cell_layer_and_scale(); - - // Convert to render cells. - let sel_rect = sel.rect.div_outward(&render_cell_layer.big_len()); - let visible_rect = self - .viewpoint() - .global_visible_rect() - .div_outward(&render_cell_layer.big_len()); - - let sel_min = sel_rect.min(); - let sel_max = sel_rect.max(); - let sel_center = sel_rect.center(); - let visible_min = visible_rect.min(); - let visible_max = visible_rect.max(); - let view_center = self.viewpoint().center().floor(); - - for &ax in Dim2D::axes() { - if sel_max[ax] < visible_min[ax].clone() + PADDING - || visible_max[ax] < sel_min[ax].clone() + PADDING - { - // Move selection to center along this axis. - sel = - sel.move_by(NdVec::unit(ax) * (view_center[ax].clone() - &sel_center[ax])); + frame.add_selection_region_highlight_overlay(&selection.rect); + } + + // Draw absolute selection resize preview after drawing selection. + if mouse.display_mode == MouseDisplayMode::ResizeSelectionToCursor { + if this.is_dragging() { + // We are already resizing the selection; just use the + // current selection. + frame.add_selection_resize_preview_overlay(&selection.rect); + } else if let (Some(resize_start), Some(resize_end)) = ( + screen_pos.and_then(|pos| pos.absolute_selection_resize_start_pos()), + rect_cell_to_highlight, + ) { + // Show what *would* happen if the user resized the + // selection. + frame.add_selection_resize_preview_overlay( + &super::selection::resize_selection_absolute( + &selection.rect, + &resize_start, + &resize_end, + ), + ); } } - self.set_selection(Some(sel)); + // Draw relative selection resize preview after drawing selection. + if let MouseDisplayMode::ResizeSelectionEdge(edge) = mouse.display_mode { + frame.add_selection_edge_resize_overlay(&selection.rect, edge); + } } - } - pub fn screen_pos(&self, pixel: FVec2D) -> ScreenPos2D { - let pos = self.viewpoint().cell_transform().pixel_to_global_pos(pixel); - let layer = self.viewpoint().render_cell_layer(); - let can_draw = !self.viewpoint().too_small_to_draw(); - ScreenPos2D { - pixel, - pos, - layer, - can_draw, - } + frame.finish() } -} -#[derive(Debug, Clone)] -pub struct ScreenPos2D { - pixel: FVec2D, - pos: FixedVec2D, - layer: Layer, - can_draw: bool, -} -impl ScreenPos2D { - /// Returns the position of the mouse in pixel space. - pub fn pixel(&self) -> FVec2D { - self.pixel - } - /// Returns the global cell position of the mouse. - pub fn pos(&self) -> FixedVec2D { - self.pos.clone() - } - /// Returns the global cell coordinates at the pixel. - pub fn cell(&self) -> BigVec2D { - self.pos.floor() - } - /// Returns the global cell rectangle of cells inside the pixel. - pub fn rect(&self) -> BigRect2D { - let render_cell_len = self.layer.big_len(); - // Round to render cell. - let base_pos = self.cell().div_floor(&render_cell_len) * render_cell_len; - self.layer.big_rect() + base_pos - } - /// Returns the global cell coordinates at the pixel, or `None` if the scale - /// is too small to draw. - pub fn draw_cell(&self) -> Option { - if self.can_draw { - Some(self.cell()) - } else { - None - } + fn screen_pos(this: &GridView2D, pixel: FVec2D) -> ScreenPos2D { + let layer = this.viewpoint().render_cell_layer(); + let pos = this.viewpoint().cell_transform().pixel_to_global_pos(pixel); + ScreenPos2D { pixel, layer, pos } } } diff --git a/ui/src/gridview/view3d.rs b/ui/src/gridview/view3d.rs index ce39d2ef..bf25f86a 100644 --- a/ui/src/gridview/view3d.rs +++ b/ui/src/gridview/view3d.rs @@ -1,151 +1,228 @@ -use anyhow::{anyhow, Context, Result}; +use anyhow::Result; use ndcell_core::prelude::*; +use super::algorithms::raycast; +use super::drag::Drag; use super::generic::{GenericGridView, GridViewDimension}; use super::render::{CellDrawParams, GridViewRender3D, RenderParams, RenderResult}; -use super::viewpoint::{CellTransform3D, Viewpoint, Viewpoint3D}; -use super::{DragHandler, DragType}; -use crate::commands::*; -use crate::math::raycast; +use super::screenpos::{RaycastHit, RaycastHitThing, ScreenPos3D, ScreenPosTrait}; +use super::viewpoint::{Viewpoint, Viewpoint3D}; +use crate::mouse::MouseDisplayMode; -pub type GridView3D = GenericGridView; +pub type GridView3D = GenericGridView; -#[derive(Debug, Default)] -pub struct GridViewDim3D; -impl GridViewDimension for GridViewDim3D { - type D = Dim3D; - type Viewpoint = Viewpoint3D; - - fn do_view_command(this: &mut GridView3D, command: ViewCommand) -> Result<()> { - // Delegate to the viewpoint. - let maybe_new_drag_handler = this - .viewpoint_interpolator - .do_view_command(command) - .context("Executing view command")?; - - // Update drag handler, if the viewpoint gave one. - if !this.is_dragging() { - if let Some(mut interpolator_drag_handler) = maybe_new_drag_handler { - this.start_drag( - DragType::MovingView, - Box::new(move |gridview: &mut GridView3D, cursor_pos| { - interpolator_drag_handler(&mut gridview.viewpoint_interpolator, cursor_pos) - }) as DragHandler, - ); - } +#[derive(Debug)] +pub struct GridViewData3D { + grid_axis: Option, + grid_coord: BigInt, +} +impl Default for GridViewData3D { + fn default() -> Self { + Self { + grid_axis: None, + grid_coord: BigInt::zero(), } - - Ok(()) } - fn do_draw_command(_this: &mut GridView3D, _command: DrawCommand) -> Result<()> { - Err(anyhow!("unimplemented")) +} +impl GridViewDimension for Dim3D { + type Viewpoint = Viewpoint3D; + type ScreenPos = ScreenPos3D; + + type Data = GridViewData3D; + + fn focus(this: &mut GridView3D, pos: &ScreenPos3D) { + if let Some(hit) = &pos.raycast_octree_hit { + // Set grid axes. + let axis = hit.face.normal_axis(); + this.show_grid(axis, hit.pos[axis].round()); + // Set position. + this.target_viewpoint().set_center(hit.pos.clone()); + } else { + // Hide grid. + this.hide_grid(); + } } - fn do_select_command(_this: &mut GridView3D, _command: SelectCommand) -> Result<()> { - Err(anyhow!("unimplemented")) + fn resize_selection_to_cursor( + rect: &BigRect3D, + start: &FixedVec3D, + end: &BigRect3D, + drag: &Drag, + ) -> BigRect3D { + super::selection::resize_selection_to_face( + &rect, + &start, + &end, + drag.initial_pos().unwrap().face, + ) } fn render(this: &mut GridView3D, params: RenderParams<'_>) -> Result { + if this.automaton.ndtree.root_ref().is_empty() && this.selection.is_none() { + // Provide a surface to place some initial cells on. + this.show_grid_at_viewpoint(Axis::Y); + } + + let mouse = params.mouse; + let screen_pos = mouse.pos.map(|pixel| this.screen_pos(pixel)); + let pos_to_highlight = this.cell_to_highlight(&mouse); + let rect_cell_to_highlight = this.render_cell_rect_to_highlight(&mouse); + let mut frame = GridViewRender3D::new(params, this.viewpoint()); + + // Draw main cells. frame.draw_cells(CellDrawParams { ndtree: &this.automaton.ndtree, alpha: 1.0, rect: None, + interactive: true, })?; - - // let hover_pos = params.cursor_pos.map(|pos| frame.pixel_pos_to_cell_pos(pos)); - // // Only allow drawing at 1:1 or bigger. - // let draw_pos = if this.interpolating_viewport.zoom.power() >= 0.0 { - // hover_pos.clone() - // } else { - // None - // }; + // Draw selection cells. + if let Some(selection) = &this.selection { + if let Some(ndtree) = &selection.cells { + frame.draw_cells(CellDrawParams { + ndtree, + alpha: 1.0, + rect: Some(&selection.rect), + interactive: false, + })?; + } + } // Draw gridlines. - frame.draw_gridlines()?; - // // Draw crosshairs. - // if let Some(pos) = &draw_pos { - // frame.draw_blue_cursor_highlight(&pos.floor()); - // } + if let Some((axis, coord)) = this.grid() { + frame.add_gridlines_overlay(axis, coord.clone()); + } + + // Draw mouse display. + if let Some(pos) = &pos_to_highlight { + match mouse.display_mode { + MouseDisplayMode::Draw(draw_mode) => { + frame.add_hover_draw_overlay(&pos.cell, pos.face, draw_mode); + } + MouseDisplayMode::Select => { + frame.add_hover_select_overlay(&pos.cell, pos.face); + } + _ => (), + } + } + + if let Some(selection) = &this.selection { + // Draw selection highlight. + if selection.cells.is_some() { + frame.add_selection_cells_highlight_overlay(&selection.rect); + } else { + frame.add_selection_region_highlight_overlay(&selection.rect); + } + + // Draw absolute selection resize preview after drawing selection. + if mouse.display_mode == MouseDisplayMode::ResizeSelectionToCursor { + if this.is_dragging() { + // We are already resizing the selection; just use the + // current selection. + frame.add_selection_resize_preview_overlay(&selection.rect); + } else if let (Some(resize_start), Some(resize_end)) = ( + screen_pos.and_then(|pos| pos.absolute_selection_resize_start_pos()), + rect_cell_to_highlight, + ) { + // Show what *would* happen if the user resized the + // selection. + frame.add_selection_resize_preview_overlay( + &super::selection::resize_selection_to_face( + &selection.rect, + &resize_start, + &resize_end, + pos_to_highlight.unwrap().face, + ), + ); + } + } + + // Draw relative selection resize preview after drawing selection. + if let MouseDisplayMode::ResizeSelectionFace(face) = mouse.display_mode { + frame.add_selection_face_resize_overlay(&selection.rect, face); + } + } frame.finish() } -} -impl GridView3D { - pub fn screen_pos(&self, pixel: FVec2D) -> ScreenPos3D { - let vp = self.viewpoint(); + + fn screen_pos(this: &GridView3D, pixel: FVec2D) -> ScreenPos3D { + let layer = this.viewpoint().render_cell_layer(); + + let vp = this.viewpoint(); let xform = vp.cell_transform(); - let mut raycast_hit; + let (start, delta) = xform.pixel_to_local_ray(pixel); + + let raycast_octree_hit; { // Get the `NdTreeSlice` containing all of the visible cells. let global_visible_rect = vp.global_visible_rect(); - let visible_octree = self.automaton.ndtree.slice_containing(&global_visible_rect); + let visible_octree = this.automaton.ndtree.slice_containing(&global_visible_rect); let local_octree_base_pos = xform.global_to_local_int(&visible_octree.base_pos).unwrap(); // Compute the ray, relative to the root node of the octree slice. - let (mut start, delta) = xform.pixel_to_local_ray(pixel); - printlnd!("octree off {}", local_octree_base_pos); - start -= local_octree_base_pos.to_fvec(); + let octree_start = start - local_octree_base_pos.to_fvec(); let layer = xform.render_cell_layer; let node = visible_octree.root.as_ref(); - raycast_hit = crate::math::raycast::octree_raycast(start, delta, layer, node); - // Convert coordinates back from octree node space into local space. - if let Some(hit) = &mut raycast_hit { - hit.start += local_octree_base_pos.to_fvec(); - hit.pos_int += local_octree_base_pos; - hit.pos_float += local_octree_base_pos.to_fvec(); - } - }; + raycast_octree_hit = raycast::intersect_octree(octree_start, delta, layer, node) + // Use `add_base_pos()` to convert coordinates back from octree + // node space into local space. + .map(|h| h.add_base_pos(local_octree_base_pos)) + .map(|h| RaycastHit::new(&xform, h, RaycastHitThing::Cell)); + } + + let raycast_gridlines_hit = this.grid().and_then(|(grid_axis, grid_coord)| { + let grid_coord = xform.global_to_local_visible_coord(grid_axis, &grid_coord)?; + raycast::intersect_plane(start, delta, grid_axis, r64(grid_coord as f64)) + .map(|h| RaycastHit::new(&xform, h, RaycastHitThing::Gridlines)) + }); + let raycast_selection_hit = None; // TODO probably remove this entirely + + // Choose the ONE RAYCAST TO RULE THEM ALL (the one that is shortest). + let raycast = [ + raycast_octree_hit.as_ref(), + raycast_gridlines_hit.as_ref(), + raycast_selection_hit.as_ref(), + ] + .iter() + .copied() + .flatten() // Remove `Option` layer. + .min() // Select minimum by distance. + .cloned(); ScreenPos3D { pixel, + layer, xform, - raycast_hit, + + raycast, + raycast_octree_hit, + raycast_gridlines_hit, + raycast_selection_hit, } } } - -#[derive(Debug, Clone)] -pub struct ScreenPos3D { - pixel: FVec2D, - xform: CellTransform3D, - raycast_hit: Option, -} -impl ScreenPos3D { - /// Returns the position of the mouse in pixel space. - pub fn pixel(&self) -> FVec2D { - self.pixel +impl GridView3D { + pub fn show_grid_at_viewpoint(&mut self, axis: Axis) { + let coord = self.target_viewpoint().center()[Axis::Y].floor(); + self.show_grid(axis, coord); } - /// Returns the global cell position at the mouse cursor in the plane parallel - /// to the screen at the distance of the viewpoint pivot. - pub fn global_pos_at_pivot_depth(&self) -> FixedVec3D { - self.xform - .pixel_to_global_pos(self.pixel, Viewpoint3D::DISTANCE_TO_PIVOT) + pub fn show_grid(&mut self, axis: Axis, coord: BigInt) { + self.dim_data.grid_axis = Some(axis); + self.dim_data.grid_coord = coord; } - - /// Returns the global position of the cell visible at the mouse cursor. - pub fn raycast(&self) -> Option { - self.raycast_hit.map(|hit| RaycastHit { - pos: self.xform.local_to_global_float(hit.pos_float), - cell: self.xform.local_to_global_int(hit.pos_int), - face: (hit.face_axis, hit.face_sign), - }) + pub fn hide_grid(&mut self) { + self.dim_data.grid_axis = None; } - /// Returns the global position of the cell visible at the mouse cursor. - pub fn raycast_face(&self) -> Option<(Axis, Sign)> { - self.raycast_hit.map(|hit| (hit.face_axis, hit.face_sign)) + pub fn grid(&self) -> Option<(Axis, &BigInt)> { + match self.dim_data.grid_axis { + Some(axis) => Some((axis, &self.dim_data.grid_coord)), + None => None, + } } } - -pub struct RaycastHit { - /// Point of intersection between ray and cell. - pub pos: FixedVec3D, - /// Position of intersected cell. - pub cell: BigVec3D, - /// Face of cell intersected. - pub face: (Axis, Sign), -} diff --git a/ui/src/gridview/viewpoint/interpolator.rs b/ui/src/gridview/viewpoint/interpolator.rs index 45b2f8a3..cbd86974 100644 --- a/ui/src/gridview/viewpoint/interpolator.rs +++ b/ui/src/gridview/viewpoint/interpolator.rs @@ -1,15 +1,13 @@ //! Viewpoint interpolation. -use anyhow::Result; -use std::any::Any; use std::fmt; use std::marker::PhantomData; use std::time::Duration; use ndcell_core::prelude::*; -use super::{DragHandler, DragOutcome, Viewpoint}; -use crate::commands::ViewCommand; +use super::{DragOutcome, DragUpdateViewFn, Viewpoint}; +use crate::commands::DragViewCmd; use crate::config::Interpolation; /// Distance beneath which to "snap" to the target, for interpolation strategies @@ -39,31 +37,18 @@ impl> From for Interpolator { } } -/// Stateful interpolation. -/// -/// This abstracts away dimensionality, so it can be used as a trait object. -pub trait Interpolate: Any { +impl> Interpolator { /// Returns the distance between the current state and the target state. - fn distance(&self) -> f64; - - /// Advances the state by one frame using the given interpolation strategy. - /// - /// Returns `true` if the target has been reached, or `false` otherwise. - fn advance(&mut self, frame_duration: Duration, interpolation: Interpolation) -> bool; - - /// Sets the display scaling factor of the underlying viewpoint. - fn set_dpi(&mut self, dpi: f32); - /// Updates the target dimensions. - fn set_target_dimensions(&mut self, target_dimensions: (u32, u32)); -} -impl> Interpolate for Interpolator { - fn distance(&self) -> f64 { + pub fn distance(&self) -> f64 { V::distance(&self.current, &self.target) .to_f64() .unwrap_or(f64::MAX) } - fn advance(&mut self, frame_duration: Duration, interpolation: Interpolation) -> bool { + /// Advances the state by one frame using the given interpolation strategy. + /// + /// Returns `true` if the target has been reached, or `false` otherwise. + pub fn advance(&mut self, frame_duration: Duration, interpolation: Interpolation) -> bool { let seconds_elapsed = frame_duration.as_secs_f64(); if self.current == self.target { @@ -90,30 +75,40 @@ impl> Interpolate for Interpolator { } } - fn set_dpi(&mut self, dpi: f32) { + /// Sets the display scaling factor of the underlying viewpoint. + pub fn set_dpi(&mut self, dpi: f32) { self.current.set_dpi(dpi); self.target.set_dpi(dpi); } - fn set_target_dimensions(&mut self, target_dimensions: (u32, u32)) { + /// Updates the target dimensions. + pub fn set_target_dimensions(&mut self, target_dimensions: (u32, u32)) { self.target.set_target_dimensions(target_dimensions); self.current.set_target_dimensions(target_dimensions); } -} -impl> Interpolator { - /// Executes a movement command, interpolating if necessary. - pub fn do_view_command(&mut self, command: ViewCommand) -> Result>> { - Ok(self - .target - .do_view_command(command)? - // Turn the DragHandler into a DragHandler>. - .map(|mut viewpoint_drag_handler| -> DragHandler { - Box::new(move |this, cursor_pos| { - // Skip interpolation for dragging. - viewpoint_drag_handler(&mut this.target, cursor_pos)?; - viewpoint_drag_handler(&mut this.current, cursor_pos)?; - Ok(DragOutcome::Continue) - }) - })) + /// Returns the drag update function for a drag view command. + pub fn make_drag_update_fn( + &self, + command: DragViewCmd, + cursor_start: FVec2D, + ) -> Option> { + match command { + DragViewCmd::Orbit3D => self.target.drag_orbit_3d(cursor_start), + DragViewCmd::Pan => self.target.drag_pan(cursor_start), + DragViewCmd::PanHorizontal3D => self.target.drag_pan_horizontal_3d(cursor_start), + DragViewCmd::Scale => self.target.drag_scale(cursor_start), + } + .map(Self::wrap_drag_update_fn) + } + + /// Wraps a drag update function to operate on an `Interpolator` instead of + /// a `Viewpoint` directly. + fn wrap_drag_update_fn(mut h: DragUpdateViewFn) -> DragUpdateViewFn { + Box::new(move |this, cursor_pos| { + // Skip interpolation for dragging. + h(&mut this.target, cursor_pos)?; + h(&mut this.current, cursor_pos)?; + Ok(DragOutcome::Continue) + }) } } diff --git a/ui/src/gridview/viewpoint/mod.rs b/ui/src/gridview/viewpoint/mod.rs index aad872cb..60a1a554 100644 --- a/ui/src/gridview/viewpoint/mod.rs +++ b/ui/src/gridview/viewpoint/mod.rs @@ -7,16 +7,19 @@ mod transform; mod viewpoint2d; mod viewpoint3d; -use crate::commands::ViewCommand; -use crate::gridview::{DragHandler, DragOutcome}; -use crate::Scale; -pub use interpolator::{Interpolate, Interpolator}; +use crate::commands::{Move2D, Move3D}; +use crate::gridview::drag::DragOutcome; +use crate::{Scale, CONFIG}; +pub use interpolator::Interpolator; pub use transform::{ CellTransform, CellTransform2D, CellTransform3D, NdCellTransform, ProjectionType, }; pub use viewpoint2d::Viewpoint2D; pub use viewpoint3d::Viewpoint3D; +/// Drag update function that modifies a viewpoint or interpolator. +type DragUpdateViewFn = Box Result>; + /// Minimum target width & height, to avoid divide-by-zero errors. const MIN_TARGET_SIZE: u32 = 10; @@ -36,7 +39,11 @@ const PIXELS_PER_2X_SCALE: f64 = 400.0; /// 2. /// /// This one is completely arbitrary. -const ROT_DEGREES_PER_2X_SCALE: f64 = PIXELS_PER_2X_SCALE; +const ROT_DEGREES_PER_2X_SCALE: f64 = 100.0; + +/// Radius of visible cells in 3D, measured in "scaled units". (See `Scale` +/// docs.) +const VIEW_RADIUS_3D: f32 = 5000.0; /// Common functionality for 2D and 3D viewpoints. pub trait Viewpoint: 'static + std::fmt::Debug + Default + Clone + PartialEq { @@ -53,9 +60,9 @@ pub trait Viewpoint: 'static + std::fmt::Debug + Default + Clone + Parti /// Returns the position the camera is looking at/from. fn center(&self) -> &FixedVec; /// Sets the position the camera is looking at/from. - fn set_pos(&mut self, pos: FixedVec); + fn set_center(&mut self, pos: FixedVec); /// Snap to the nearest integer cell position. - fn snap_pos(&mut self); + fn snap_center(&mut self); /// Returns the visual scale of cells. fn scale(&self) -> Scale; @@ -88,7 +95,7 @@ pub trait Viewpoint: 'static + std::fmt::Debug + Default + Clone + Parti // Apply that offset so that the point goes back to the same scaled // location as before. - self.set_pos(self.center() + new_scale.units_to_cells(delta_pixel_offset)); + self.set_center(self.center() + new_scale.units_to_cells(delta_pixel_offset)); } /// Scales by 2^`log2_factor`, keeping one invariant point at the same /// location on the screen. @@ -123,11 +130,6 @@ pub trait Viewpoint: 'static + std::fmt::Debug + Default + Clone + Parti self.scale_by_factor(self.scale().round() / self.scale(), invariant_pos); self.set_scale(self.scale().round()); // Fix any potential rounding error. } - /// Returns `true` if the scale is too small to draw individual cells, or - /// `false` otherwise. - fn too_small_to_draw(&self) -> bool { - self.scale() < Scale::from_factor(r64(1.0)) - } /// Returns the abstract "distance" between two viewpoints. fn distance(a: &Self, b: &Self) -> FixedPoint { @@ -188,8 +190,27 @@ pub trait Viewpoint: 'static + std::fmt::Debug + Default + Clone + Parti /// rounded outward to the nearest render cell. fn global_visible_rect(&self) -> BigRect; - /// Executes a movement command. - fn do_view_command(&mut self, command: ViewCommand) -> Result>>; + /// Returns a drag update function for `DragViewCmd::Orbit3D`. + fn drag_orbit_3d(&self, cursor_start: FVec2D) -> Option>; + /// Returns a drag update function for `DragViewCmd::Pan`. + fn drag_pan(&self, cursor_start: FVec2D) -> Option>; + /// Returns a drag update function for `DragViewCmd::PanHorizontal3D`. + fn drag_pan_horizontal_3d(&self, cursor_start: FVec2D) -> Option>; + /// Returns a drag update function for `DragViewCmd::Scale`. + fn drag_scale(&self, cursor_start: FVec2D) -> Option> { + let initial_scale = self.scale(); + Some(Box::new(move |this, cursor_end| { + let delta = + (cursor_end - cursor_start)[Axis::Y] / -CONFIG.lock().ctrl.pixels_per_2x_scale_2d; + this.set_scale(Scale::from_log2_factor(initial_scale.log2_factor() + delta)); + Ok(DragOutcome::Continue) + })) + } + + /// Moves the viewpoint in 2D. + fn apply_move_2d(&mut self, movement: Move2D); + /// Moves the viewpoint in 3D. + fn apply_move_3d(&mut self, movement: Move3D); } /// Returns the "average" scale between the two viewpoints, averaging scale diff --git a/ui/src/gridview/viewpoint/transform.rs b/ui/src/gridview/viewpoint/transform.rs index f181a1b3..cc8f683a 100644 --- a/ui/src/gridview/viewpoint/transform.rs +++ b/ui/src/gridview/viewpoint/transform.rs @@ -5,7 +5,7 @@ use std::convert::TryFrom; use ndcell_core::prelude::*; use crate::ext::*; -use crate::Scale; +use crate::{Plane, Scale}; pub type CellTransform2D = NdCellTransform; pub type CellTransform3D = NdCellTransform; @@ -284,6 +284,24 @@ impl NdCellTransform { } Some(IRect::span(min, max)) } + + /// Converts a single coordinate from global space to local space, if the + /// point is inside the visible area. Returns `None` if the point is outside + /// the visible area. + pub fn global_to_local_visible_coord(&self, axis: Axis, coordinate: &BigInt) -> Option { + let big_local_coord = (coordinate - &self.origin[axis]) >> self.render_cell_layer.to_u32(); + if let Some(local_coord) = big_local_coord.to_isize() { + let cell_view_radius = + self.render_cell_scale.cells_per_unit().raw() as f32 * super::VIEW_RADIUS_3D; + if local_coord.abs() < cell_view_radius.ceil() as isize { + Some(local_coord) + } else { + None // The coordinate is outside the visible area. + } + } else { + None // The coordinate is outside the visible area. + } + } } impl CellTransform2D { @@ -381,21 +399,16 @@ impl CellTransform3D { /// Returns the global position on an axis-aligned plane that appears at the /// given pixel position on the screen. - pub fn pixel_to_global_pos_in_plane( - &self, - pixel: FVec2D, - plane: (Axis, &FixedPoint), - ) -> Option { - let (plane_axis, plane_pos) = plane; + pub fn pixel_to_global_pos_in_plane(&self, pixel: FVec2D, plane: &Plane) -> Option { let (start, delta) = self.pixel_to_global_ray(pixel); - if delta[plane_axis].is_zero() { + if delta[plane.axis].is_zero() { // The delta vector is parallel to the plane. return None; } // How many times do we have to add `delta` to reach the plane? - let t = (plane_pos - &start[plane_axis]) / &delta[plane_axis]; + let t = (&plane.coordinate - &start[plane.axis]) / &delta[plane.axis]; if !t.is_positive() { // The plane is behind the camera. diff --git a/ui/src/gridview/viewpoint/viewpoint2d.rs b/ui/src/gridview/viewpoint/viewpoint2d.rs index 6fba87e1..d74a515c 100644 --- a/ui/src/gridview/viewpoint/viewpoint2d.rs +++ b/ui/src/gridview/viewpoint/viewpoint2d.rs @@ -1,4 +1,3 @@ -use anyhow::{anyhow, Context, Result}; use cgmath::{Matrix4, SquareMatrix}; use log::warn; @@ -6,11 +5,18 @@ use ndcell_core::prelude::*; use Axis::{X, Y}; use super::{ - CellTransform2D, DragHandler, DragOutcome, ProjectionType, Scale, Viewpoint, MIN_TARGET_SIZE, + CellTransform2D, DragOutcome, DragUpdateViewFn, ProjectionType, Scale, Viewpoint, + MIN_TARGET_SIZE, }; -use crate::commands::{ViewCommand, ViewDragCommand}; +use crate::commands::{Cmd, DragViewCmd, Move2D, Move3D}; use crate::CONFIG; +macro_rules! ignore_command { + ($c:expr) => { + warn!("Ignoring {:?} in Viewpoint2D", $c); + }; +} + #[derive(Debug, Clone, PartialEq)] pub struct Viewpoint2D { /// Width and height of the render target. @@ -56,10 +62,10 @@ impl Viewpoint for Viewpoint2D { fn center(&self) -> &FixedVec { &self.center } - fn set_pos(&mut self, pos: FixedVec) { + fn set_center(&mut self, pos: FixedVec) { self.center = pos; } - fn snap_pos(&mut self) { + fn snap_center(&mut self) { if CONFIG.lock().ctrl.snap_center_2d { self.center = self.center.floor().to_fixedvec() + 0.5; } else { @@ -138,10 +144,10 @@ impl Viewpoint for Viewpoint2D { // cells boundaries line up with pixel boundaries. let (target_w, target_h) = self.target_dimensions(); if target_w % 2 == 1 { - center_in_pixel_units[X] -= 0.5_f64; + center_in_pixel_units[X] += 0.5_f64; } if target_h % 2 == 1 { - center_in_pixel_units[Y] -= 0.5_f64; + center_in_pixel_units[Y] += 0.5_f64; } self.scale.units_to_cells(center_in_pixel_units) } else { @@ -178,86 +184,28 @@ impl Viewpoint for Viewpoint2D { )) } - fn do_view_command(&mut self, command: ViewCommand) -> Result>> { - match command { - ViewCommand::Drag(c, cursor_start) => match c { - ViewDragCommand::Orbit => { - warn!("Ignoring {:?} in Viewpoint2D", command); - Ok(None) - } - - ViewDragCommand::Pan - | ViewDragCommand::PanAligned - | ViewDragCommand::PanAlignedVertical - | ViewDragCommand::PanHorizontal => { - let start = self.cell_transform().pixel_to_global_pos(cursor_start); - Ok(Some(Box::new(move |this, cursor_end| { - let end = this.cell_transform().pixel_to_global_pos(cursor_end); - this.center += start.clone() - end; - Ok(DragOutcome::Continue) - }))) - } - - ViewDragCommand::Scale => todo!("Scale using click & drag"), - }, - - ViewCommand::GoTo2D { - mut x, - mut y, - relative, - scaled, - } => { - if scaled { - x = x.map(|x| self.scale.units_to_cells(x)); - y = y.map(|y| self.scale.units_to_cells(y)); - } - if relative { - self.center[X] += x.unwrap_or_default(); - self.center[Y] += y.unwrap_or_default(); - } else { - if let Some(x) = x { - self.center[X] = x; - } - if let Some(y) = y { - self.center[Y] = y; - } - } - Ok(None) - } - ViewCommand::GoTo3D { .. } => { - warn!("Ignoring {:?} in Viewpoint2D", command); - Ok(None) - } - ViewCommand::GoToScale(scale) => { - self.set_scale(scale); - Ok(None) - } - - ViewCommand::Scale { - log2_factor, - invariant_pos, - } => { - self.scale_by_log2_factor( - R64::try_new(log2_factor).context("Invalid scale factor")?, - invariant_pos.map(|pixel| self.cell_transform().pixel_to_global_pos(pixel)), - ); - Ok(None) - } - - ViewCommand::SnapPos => { - self.snap_pos(); - Ok(None) - } - ViewCommand::SnapScale { invariant_pos } => { - self.snap_scale( - invariant_pos.map(|pixel| self.cell_transform().pixel_to_global_pos(pixel)), - ); - Ok(None) - } + fn drag_orbit_3d(&self, _cursor_start: FVec2D) -> Option> { + ignore_command!(DragViewCmd::Orbit3D); + None + } + fn drag_pan(&self, cursor_start: FVec2D) -> Option> { + let start = self.cell_transform().pixel_to_global_pos(cursor_start); + Some(Box::new(move |this, cursor_end| { + let end = this.cell_transform().pixel_to_global_pos(cursor_end); + this.center += start.clone() - end; + Ok(DragOutcome::Continue) + })) + } + fn drag_pan_horizontal_3d(&self, cursor_start: FVec2D) -> Option> { + self.drag_pan(cursor_start) + } - ViewCommand::FitView => Err(anyhow!( - "FitView command received in Viewpoint2D (must be converted to GoTo command)" - )), - } + fn apply_move_2d(&mut self, movement: Move2D) { + let Move2D { dx, dy } = movement; + let delta: FVec2D = NdVec([r64(dx), r64(dy)]); + self.center += self.scale.units_to_cells(delta.to_fixedvec()); + } + fn apply_move_3d(&mut self, movement: Move3D) { + ignore_command!(Cmd::Move3D(movement)); } } diff --git a/ui/src/gridview/viewpoint/viewpoint3d.rs b/ui/src/gridview/viewpoint/viewpoint3d.rs index 02b8c658..444e9d9e 100644 --- a/ui/src/gridview/viewpoint/viewpoint3d.rs +++ b/ui/src/gridview/viewpoint/viewpoint3d.rs @@ -1,4 +1,3 @@ -use anyhow::Result; use cgmath::prelude::*; use cgmath::{Basis3, Decomposed, Deg, Matrix4}; use log::warn; @@ -7,11 +6,18 @@ use ndcell_core::prelude::*; use Axis::{X, Y, Z}; use super::{ - CellTransform3D, DragHandler, DragOutcome, ProjectionType, Scale, Viewpoint, MIN_TARGET_SIZE, + CellTransform3D, DragOutcome, DragUpdateViewFn, ProjectionType, Scale, Viewpoint, + MIN_TARGET_SIZE, }; -use crate::commands::{ViewCommand, ViewDragCommand}; +use crate::commands::{Cmd, Move2D, Move3D}; use crate::config::{ForwardAxis3D, UpAxis3D}; -use crate::CONFIG; +use crate::{Plane, CONFIG}; + +macro_rules! ignore_command { + ($c:expr) => { + warn!("Ignoring {:?} in Viewpoint3D", $c); + }; +} #[derive(Debug, Clone, PartialEq)] pub struct Viewpoint3D { @@ -52,7 +58,7 @@ impl Viewpoint3D { pub const DEFAULT_YAW: Deg = Deg(20.0); /// Radius of visible cells, measured in "scaled units". (See `Scale` docs.) - pub const VIEW_RADIUS: f32 = 5000.0; + pub const VIEW_RADIUS: f32 = super::VIEW_RADIUS_3D; /// Returns the yaw of the camera. pub fn yaw(&self) -> Deg { @@ -63,6 +69,10 @@ impl Viewpoint3D { // Clamp yaw to -180..+180. self.yaw = yaw.normalize_signed(); } + /// Adds yaw to the camera. + pub fn add_yaw(&mut self, dyaw: Deg) { + self.set_yaw(self.yaw + dyaw); + } /// Returns the pitch of the camera. pub fn pitch(&self) -> Deg { self.pitch @@ -70,13 +80,11 @@ impl Viewpoint3D { /// Sets the pitch of the camera. pub fn set_pitch(&mut self, pitch: Deg) { // Clamp pitch to -90..+90. - self.pitch = pitch.normalize_signed(); - if self.pitch > Deg(90.0) { - self.pitch = Deg(90.0); - } - if self.pitch < Deg(-90.0) { - self.pitch = Deg(-90.0); - } + self.pitch = Deg(pitch.0.clamp(-90.0, 90.0)); + } + /// Adds pitch to the camera. + pub fn add_pitch(&mut self, dpitch: Deg) { + self.set_pitch(self.pitch + dpitch); } /// Returns the orientation of the camera. @@ -93,7 +101,7 @@ impl Viewpoint3D { pub fn look_vector(&self) -> FVec3D { fvec3d_from_basis3(Z, self.orientation()) } - /// Returns the position of the camera (not the pivot). + /// Returns the global position of the camera (not the pivot). pub fn camera_pos(&self) -> FixedVec3D { let distance = self.scale.inv_factor() * FixedPoint::from(r64(Self::DISTANCE_TO_PIVOT as f64)); @@ -155,10 +163,10 @@ impl Viewpoint for Viewpoint3D { fn center(&self) -> &FixedVec { &self.pivot } - fn set_pos(&mut self, pos: FixedVec) { + fn set_center(&mut self, pos: FixedVec) { self.pivot = pos } - fn snap_pos(&mut self) { + fn snap_center(&mut self) { self.pivot = self.pivot.round().to_fixedvec(); } @@ -238,129 +246,69 @@ impl Viewpoint for Viewpoint3D { render_cell_layer.round_rect(&BigRect3D::centered(pivot, &cell_radius)) } - fn do_view_command(&mut self, command: ViewCommand) -> Result>> { + fn drag_orbit_3d(&self, cursor_start: FVec2D) -> Option> { let config = CONFIG.lock(); - - match command { - ViewCommand::Drag(c, cursor_start) => match c { - ViewDragCommand::Orbit => { - let orbit_factor = r64(config.ctrl.mouse_orbit_speed / config.gfx.dpi); - let old_yaw = self.yaw(); - let old_pitch = self.pitch(); - Ok(Some(Box::new(move |this, cursor_end| { - let delta = (cursor_end - cursor_start) * orbit_factor; - this.set_yaw(old_yaw + Deg(delta[X].raw() as f32)); - this.set_pitch(old_pitch + Deg(delta[Y].raw() as f32)); - Ok(DragOutcome::Continue) - }))) - } - - ViewDragCommand::Pan => { - let z = Self::DISTANCE_TO_PIVOT; - let start = self.cell_transform().pixel_to_global_pos(cursor_start, z); - Ok(Some(Box::new(move |this, cursor_end| { - let end = this.cell_transform().pixel_to_global_pos(cursor_end, z); - this.pivot += &start - &end; - Ok(DragOutcome::Continue) - }))) - } - ViewDragCommand::PanAligned => { - todo!("pan aligned"); - } - ViewDragCommand::PanAlignedVertical => { - todo!("pan aligned vertical"); - } - ViewDragCommand::PanHorizontal => { - let y = self.pivot[Y].clone(); - let start = self - .cell_transform() - .pixel_to_global_pos_in_plane(cursor_start, (Y, &y)); - Ok(Some(Box::new(move |this, cursor_end| { - let end = this - .cell_transform() - .pixel_to_global_pos_in_plane(cursor_end, (Y, &y)); - if let (Some(start), Some(end)) = (&start, &end) { - this.pivot += start - end; - } - Ok(DragOutcome::Continue) - }))) - } - - ViewDragCommand::Scale => todo!("Scale using click & drag"), - }, - - ViewCommand::GoTo2D { .. } => { - warn!("Ignoring {:?} in Viewpoint3D", command); - Ok(None) - } - ViewCommand::GoTo3D { - mut x, - mut y, - mut z, - yaw, - pitch, - relative, - scaled, - } => { - if scaled { - x = x.map(|x| self.scale.units_to_cells(x)); - y = y.map(|y| self.scale.units_to_cells(y)); - z = z.map(|y| self.scale.units_to_cells(y)); - } - if relative { - let right = self.right_vector(config.ctrl.fwd_axis_3d); - let up = self.up_vector(config.ctrl.up_axis_3d); - let fwd = self.forward_vector(config.ctrl.fwd_axis_3d); - self.pivot += right.to_fixedvec() * x.unwrap_or_default(); - self.pivot += up.to_fixedvec() * y.unwrap_or_default(); - self.pivot += fwd.to_fixedvec() * z.unwrap_or_default(); - self.set_yaw(self.yaw() + pitch.unwrap_or(Deg(0.0))); - self.set_pitch(self.pitch() + yaw.unwrap_or(Deg(0.0))); - } else { - if let Some(x) = x { - self.pivot[X] = x; - } - if let Some(y) = y { - self.pivot[Y] = y; - } - if let Some(z) = z { - self.pivot[Z] = z; - } - if let Some(yaw) = yaw { - self.set_yaw(yaw); - } - if let Some(pitch) = pitch { - self.set_pitch(pitch); - } - } - Ok(None) - } - ViewCommand::GoToScale(scale) => { - self.set_scale(scale); - Ok(None) - } - - ViewCommand::Scale { - log2_factor, - invariant_pos: _, - } => { - self.scale_by_log2_factor(r64(log2_factor), None); - Ok(None) - } - - ViewCommand::SnapPos => { - self.snap_pos(); - Ok(None) - } - ViewCommand::SnapScale { invariant_pos: _ } => { - self.snap_scale(None); - Ok(None) + let orbit_factor = r64(config.ctrl.mouse_orbit_speed / config.gfx.dpi); + let old_yaw = self.yaw(); + let old_pitch = self.pitch(); + Some(Box::new(move |this, cursor_end| { + let delta = (cursor_end - cursor_start) * orbit_factor; + this.set_yaw(old_yaw + Deg(delta[X].raw() as f32)); + this.set_pitch(old_pitch + Deg(delta[Y].raw() as f32)); + Ok(DragOutcome::Continue) + })) + } + fn drag_pan(&self, cursor_start: FVec2D) -> Option> { + let z = Self::DISTANCE_TO_PIVOT; + let start = self.cell_transform().pixel_to_global_pos(cursor_start, z); + Some(Box::new(move |this, cursor_end| { + let end = this.cell_transform().pixel_to_global_pos(cursor_end, z); + this.pivot += &start - &end; + Ok(DragOutcome::Continue) + })) + } + fn drag_pan_horizontal_3d(&self, cursor_start: FVec2D) -> Option> { + let plane = Plane { + axis: Y, + coordinate: self.pivot[Y].clone(), + }; + let start = self + .cell_transform() + .pixel_to_global_pos_in_plane(cursor_start, &plane); + Some(Box::new(move |this, cursor_end| { + let end = this + .cell_transform() + .pixel_to_global_pos_in_plane(cursor_end, &plane); + if let (Some(start), Some(end)) = (&start, &end) { + this.pivot += start - end; } + Ok(DragOutcome::Continue) + })) + } - ViewCommand::FitView => { - todo!("fit view 3D"); - } - } + fn apply_move_2d(&mut self, movement: Move2D) { + ignore_command!(Cmd::Move2D(movement)); + } + fn apply_move_3d(&mut self, movement: Move3D) { + let Move3D { + dx, + dy, + dz, + dpitch, + dyaw, + } = movement; + let delta = cgmath::vec3(dx, dy, dz); + let delta = delta.cast::().unwrap_or(cgmath::vec3(0.0, 0.0, 0.0)); + let delta = self.flat_orientation().invert().rotate_vector(delta); + let delta = delta.cast::().unwrap_or(cgmath::vec3(0.0, 0.0, 0.0)); + let delta: FVec3D = NdVec([ + r64(delta.x as f64), + r64(delta.y as f64), + r64(delta.z as f64), + ]); + self.pivot += self.scale.units_to_cells(delta.to_fixedvec()); + self.add_pitch(dpitch); + self.add_yaw(dyaw); } } diff --git a/ui/src/gridview/worker.rs b/ui/src/gridview/worker.rs index a425e318..d497fa7e 100644 --- a/ui/src/gridview/worker.rs +++ b/ui/src/gridview/worker.rs @@ -152,7 +152,7 @@ impl WorkerThread { let mut state = lock.lock(); loop { match std::mem::replace(&mut *state, State::Idle) { - s @ State::Idle | s @ State::Done { .. } => { + s @ State::Idle | s @ State::Done(_) => { *state = s; // Wait for the main thread to send more work. condvar.wait(&mut state); @@ -166,7 +166,7 @@ impl WorkerThread { *state = State::Done(result); } } - State::Working { .. } => { + State::Working(_) => { // This should not happen, because the `Working` state // should only exist while this thread is doing work. error!("Worker thread woken in state {:?}", state); diff --git a/ui/src/gui.rs b/ui/src/gui.rs index 7b741bcb..21f156a6 100644 --- a/ui/src/gui.rs +++ b/ui/src/gui.rs @@ -1,8 +1,9 @@ +use anyhow::{Context, Result}; use glium::glutin::event::{Event, StartCause, WindowEvent}; use glium::glutin::event_loop::{ControlFlow, EventLoop}; use glium::glutin::{window::WindowBuilder, ContextBuilder}; use glium::Surface; -use imgui::{Context, FontSource}; +use imgui::FontSource; use imgui_glium_renderer::Renderer; use imgui_winit_support::{HiDpiMode, WinitPlatform}; use send_wrapper::SendWrapper; @@ -10,7 +11,7 @@ use std::cell::RefCell; use std::collections::VecDeque; use std::time::Instant; -use crate::clipboard_compat::*; +use crate::clipboard_compat::ClipboardCompat; use crate::gridview; use crate::input; use crate::windows; @@ -21,7 +22,9 @@ lazy_static! { SendWrapper::new(RefCell::new(Some(EventLoop::new()))); pub static ref DISPLAY: SendWrapper = SendWrapper::new({ let wb = WindowBuilder::new().with_title(super::TITLE.to_owned()); - let cb = ContextBuilder::new().with_vsync(true); + let cb = ContextBuilder::new() + .with_vsync(true) + .with_multisampling(CONFIG.lock().gfx.msaa as u16); glium::Display::new(wb, cb, EVENT_LOOP.borrow().as_ref().unwrap()) .expect("Failed to initialize display") }); @@ -38,7 +41,7 @@ pub fn show_gui() -> ! { let mut events_buffer = VecDeque::new(); // Initialize imgui. - let mut imgui = Context::create(); + let mut imgui = imgui::Context::create(); imgui.set_clipboard_backend(Box::new(ClipboardCompat)); imgui.set_ini_filename(None); let mut platform = WinitPlatform::init(&mut imgui); @@ -146,31 +149,44 @@ pub fn show_gui() -> ! { gridview: &mut gridview, }); if !imgui_has_mouse { - ui.set_mouse_cursor(input_state.mouse().display.cursor_icon()); + ui.set_mouse_cursor(input_state.mouse().display_mode.cursor_icon()); } let mut target = display.draw(); - // Execute commands and run the simulation. - gridview.do_frame().expect("Unhandled exception!"); - - if target.get_dimensions() != (0, 0) { - // Render the gridview. - gridview - .render(gridview::RenderParams { - target: &mut target, - mouse: input_state.mouse(), - modifiers: input_state.modifiers(), - }) - .expect("Unhandled exception!"); - - // Render imgui. - platform.prepare_render(&ui, gl_window.window()); - let draw_data = ui.render(); - renderer - .render(&mut target, draw_data) - .expect("Rendering failed"); - } + // Use IIFE for error handling. + let gridview_frame_result = || -> Result<()> { + // Execute commands and run the simulation. + gridview.do_frame().context("Updating gridview")?; + + if target.get_dimensions() != (0, 0) { + // Render the gridview. + gridview + .render(gridview::RenderParams { + target: &mut target, + mouse: input_state.mouse(), + modifiers: input_state.modifiers(), + }) + .context("Rendering gridview")?; + } + + Ok(()) + }(); + + // Handle gridview errors. + windows::error_popup::show_error_popup_on_error( + &ui, + &gridview, + gridview_frame_result, + ); + + // Render imgui. + platform.prepare_render(&ui, gl_window.window()); + let draw_data = ui.render(); + renderer + .render(&mut target, draw_data) + .expect("Error while rendering imgui"); + // Put it all on the screen. target.finish().expect("Failed to swap buffers"); diff --git a/ui/src/input.rs b/ui/src/input.rs index 998e7a95..e3e6c611 100644 --- a/ui/src/input.rs +++ b/ui/src/input.rs @@ -2,7 +2,6 @@ use glium::glutin::dpi::{PhysicalPosition, PhysicalSize}; use glium::glutin::event::*; use imgui_winit_support::WinitPlatform; use std::collections::HashSet; -use std::fmt; use std::ops::Index; use std::time::{Duration, Instant}; @@ -10,8 +9,7 @@ use ndcell_core::prelude::*; use crate::commands::*; use crate::gridview::GridView; -use crate::mouse::{MouseDisplay, MouseState}; -use crate::Scale; +use crate::mouse::{MouseDisplayMode, MouseState}; use crate::CONFIG; const SHIFT: ModifiersState = ModifiersState::SHIFT; @@ -32,6 +30,7 @@ mod sc { pub const S: u32 = 1; pub const D: u32 = 2; pub const Q: u32 = 12; + pub const E: u32 = 14; pub const Z: u32 = 6; } #[cfg(not(any(target_os = "macos")))] @@ -41,6 +40,7 @@ mod sc { pub const S: u32 = 31; pub const D: u32 = 32; pub const Q: u32 = 16; + pub const E: u32 = 18; pub const Z: u32 = 44; } @@ -150,19 +150,22 @@ impl FrameInProgress<'_> { config.ctrl.discrete_scale_speed_2d } (GridView::View2D(_), MouseScrollDelta::PixelDelta(_)) => { - config.ctrl.smooth_scroll_speed_2d + 1.0 / config.ctrl.pixels_per_2x_scale_2d } (GridView::View3D(_), MouseScrollDelta::LineDelta(_, _)) => { config.ctrl.discrete_scale_speed_3d } (GridView::View3D(_), MouseScrollDelta::PixelDelta(_)) => { - config.ctrl.smooth_scroll_speed_3d + 1.0 / config.ctrl.pixels_per_2x_scale_3d } }; - self.gridview.enqueue(ViewCommand::Scale { - log2_factor: dy * speed, - invariant_pos: self.state.mouse.pos, - }); + let log2_scale_factor = dy * speed; + if let Some(mouse_pos) = self.state.mouse.pos { + self.gridview + .enqueue(Cmd::ScaleToCursor(log2_scale_factor).at(mouse_pos)); + } else { + self.gridview.enqueue(Cmd::Scale(log2_scale_factor)); + } self.state.scale_snap_cooldown = Some(Instant::now() + SCALE_SNAP_COOLDOWN); } WindowEvent::MouseInput { button, state, .. } => match state { @@ -188,6 +191,7 @@ impl FrameInProgress<'_> { KeyboardInput { state: ElementState::Pressed, virtual_keycode, + scancode, .. } => { // We don't care about left vs. right modifiers, so just extract @@ -196,30 +200,22 @@ impl FrameInProgress<'_> { if modifiers.is_empty() { match virtual_keycode { - Some(VirtualKeyCode::Space) => { - self.gridview.enqueue(SimCommand::Step(1.into())) - } - Some(VirtualKeyCode::Tab) => { - self.gridview.enqueue(SimCommand::StepStepSize) - } - Some(VirtualKeyCode::Return) => { - self.gridview.enqueue(SimCommand::ToggleRunning) - } - Some(VirtualKeyCode::Escape) => self.gridview.enqueue(Command::Cancel), + Some(VirtualKeyCode::Space) => self.gridview.enqueue(Cmd::Step(1)), + Some(VirtualKeyCode::Tab) => self.gridview.enqueue(Cmd::StepStepSize), + Some(VirtualKeyCode::Return) => self.gridview.enqueue(Cmd::ToggleRunning), + Some(VirtualKeyCode::Escape) => self.gridview.enqueue(Cmd::Cancel), Some(VirtualKeyCode::Equals) | Some(VirtualKeyCode::NumpadAdd) => { config.sim.step_size *= 2; - self.gridview.enqueue(SimCommand::UpdateStepSize); + self.gridview.enqueue(Cmd::UpdateStepSize); } Some(VirtualKeyCode::Minus) | Some(VirtualKeyCode::NumpadSubtract) => { config.sim.step_size /= 2; if config.sim.step_size < 1.into() { config.sim.step_size = 1.into(); } - self.gridview.enqueue(SimCommand::UpdateStepSize); - } - Some(VirtualKeyCode::Delete) => { - self.gridview.enqueue(SelectCommand::Delete) + self.gridview.enqueue(Cmd::UpdateStepSize); } + Some(VirtualKeyCode::Delete) => self.gridview.enqueue(Cmd::DeleteSelection), Some(k @ VirtualKeyCode::LBracket) | Some(k @ VirtualKeyCode::RBracket) | Some(k @ VirtualKeyCode::Key0) @@ -232,7 +228,7 @@ impl FrameInProgress<'_> { | Some(k @ VirtualKeyCode::Key7) | Some(k @ VirtualKeyCode::Key8) | Some(k @ VirtualKeyCode::Key9) => { - self.gridview.enqueue(DrawCommand::SetState(match k { + self.gridview.enqueue(Cmd::SetDrawState(match k { // Select the previous cell state. VirtualKeyCode::LBracket => { self.gridview.selected_cell_state().wrapping_sub(1) @@ -254,57 +250,42 @@ impl FrameInProgress<'_> { _ => unreachable!(), })); } - _ => (), + _ => match *scancode { + sc::E => { + if let Some(mouse_pos) = self.state.mouse.pos { + self.gridview.enqueue(Cmd::FocusCursor.at(mouse_pos)); + } + } + _ => (), + }, } } if modifiers == CTRL { match virtual_keycode { // Select all. - Some(VirtualKeyCode::A) => self.gridview.enqueue(SelectCommand::SelectAll), + Some(VirtualKeyCode::A) => self.gridview.enqueue(Cmd::SelectAll), // Undo. - Some(VirtualKeyCode::Z) => self.gridview.enqueue(HistoryCommand::Undo), + Some(VirtualKeyCode::Z) => self.gridview.enqueue(Cmd::Undo), // Redo. - Some(VirtualKeyCode::Y) => self.gridview.enqueue(HistoryCommand::Redo), + Some(VirtualKeyCode::Y) => self.gridview.enqueue(Cmd::Redo), // Reset. - Some(VirtualKeyCode::R) => { - self.gridview.enqueue(HistoryCommand::UndoTo(0.into())) - } + Some(VirtualKeyCode::R) => self.gridview.enqueue(Cmd::Reset), // Cut RLE. Some(VirtualKeyCode::X) => { - self.gridview.enqueue(SelectCommand::Copy(CaFormat::Rle)); - self.gridview.enqueue(SelectCommand::Delete); + self.gridview.enqueue(Cmd::CopySelection(CaFormat::Rle)); + self.gridview.enqueue(Cmd::DeleteSelection); } // Copy RLE. Some(VirtualKeyCode::C) => { - self.gridview.enqueue(SelectCommand::Copy(CaFormat::Rle)) + self.gridview.enqueue(Cmd::CopySelection(CaFormat::Rle)); } // Paste. - Some(VirtualKeyCode::V) => self.gridview.enqueue(SelectCommand::Paste), + Some(VirtualKeyCode::V) => self.gridview.enqueue(Cmd::PasteSelection), // Center view. - Some(VirtualKeyCode::M) => { - self.gridview.enqueue(match self.gridview { - GridView::View2D(_) => ViewCommand::GoTo2D { - x: Some(r64(0.0).into()), - y: Some(r64(0.0).into()), - relative: false, - scaled: false, - }, - GridView::View3D(_) => ViewCommand::GoTo3D { - x: Some(r64(0.0).into()), - y: Some(r64(0.0).into()), - z: Some(r64(0.0).into()), - yaw: Some(crate::gridview::Viewpoint3D::DEFAULT_YAW.into()), - pitch: Some(crate::gridview::Viewpoint3D::DEFAULT_PITCH.into()), - relative: false, - scaled: false, - }, - }); - self.gridview - .enqueue(ViewCommand::GoToScale(Scale::default())); - } + Some(VirtualKeyCode::M) => self.gridview.enqueue(Cmd::ResetView), // Fit view to pattern. - Some(VirtualKeyCode::F) => self.gridview.enqueue(ViewCommand::FitView), + Some(VirtualKeyCode::F) => self.gridview.enqueue(Cmd::FitView), _ => (), } } @@ -312,17 +293,18 @@ impl FrameInProgress<'_> { if modifiers == SHIFT | CTRL { match virtual_keycode { // Redo. - Some(VirtualKeyCode::Z) => self.gridview.enqueue(HistoryCommand::Redo), + Some(VirtualKeyCode::Z) => self.gridview.enqueue(Cmd::Redo), // Cut Macrocell. Some(VirtualKeyCode::X) => { self.gridview - .enqueue(SelectCommand::Copy(CaFormat::Macrocell)); - self.gridview.enqueue(SelectCommand::Delete); + .enqueue(Cmd::CopySelection(CaFormat::Macrocell)); + self.gridview.enqueue(Cmd::DeleteSelection); } // Copy Macrocell. - Some(VirtualKeyCode::C) => self - .gridview - .enqueue(SelectCommand::Copy(CaFormat::Macrocell)), + Some(VirtualKeyCode::C) => { + self.gridview + .enqueue(Cmd::CopySelection(CaFormat::Macrocell)); + } // Reload shaders (debug build only). #[cfg(debug_assertions)] @@ -346,8 +328,8 @@ impl FrameInProgress<'_> { } // Ignore mouse press if we don't own the mouse cursor. - let cursor_pos = match self.state.mouse.pos { - Some(pos) => pos, + let mouse_pos = match self.state.mouse.pos { + Some(mouse_pos) => mouse_pos, None => return, }; @@ -359,12 +341,11 @@ impl FrameInProgress<'_> { if button == MouseButton::Left && maybe_mouse_target.is_some() { // Possibility #1: Drag mouse target (left mouse button only) let mouse_target_data = maybe_mouse_target.unwrap(); - // Possibility #1: Drag mouse target - if let Some(b) = &mouse_target_data.binding { - self.state.dragging_button = Some(button); - self.state.mouse.display = mouse_target_data.display; - self.gridview.enqueue(b.to_command(cursor_pos)); - } + let binding = &mouse_target_data.binding; + self.state.dragging_button = Some(button); + self.state.mouse.display_mode = binding.mouse_display_mode(); + self.gridview + .enqueue(Cmd::BeginDrag(binding.clone()).at(mouse_pos)); } else if let Some(b) = click_binding { // Possibility #2: Click match b { @@ -375,13 +356,14 @@ impl FrameInProgress<'_> { } else if let Some(b) = drag_binding { // Possibility #3: Drag self.state.dragging_button = Some(button); - self.state.mouse.display = b.display(); - self.gridview.enqueue(b.to_command(cursor_pos)); + self.state.mouse.display_mode = b.mouse_display_mode(); + self.gridview + .enqueue(Cmd::BeginDrag(b.clone()).at(mouse_pos)); } } fn handle_mouse_release(&mut self, button: MouseButton) { if self.state.dragging_button == Some(button) { - self.gridview.enqueue(Command::StopDrag); + self.gridview.enqueue(Cmd::EndDrag); self.state.dragging_button = None; } } @@ -398,12 +380,12 @@ impl FrameInProgress<'_> { MouseButton::Left, ); if let Some(mouse_target_data) = &self.gridview.last_render_result().mouse_target { - self.state.mouse.display = mouse_target_data.display; + self.state.mouse.display_mode = mouse_target_data.binding.mouse_display_mode(); } else { - self.state.mouse.display = None - .or(click_binding.as_ref().map(|b| b.display())) - .or(drag_binding.as_ref().map(|b| b.display())) - .unwrap_or(MouseDisplay::Normal); + self.state.mouse.display_mode = None + .or(click_binding.as_ref().map(|b| b.mouse_display_mode())) + .or(drag_binding.as_ref().map(|b| b.mouse_display_mode())) + .unwrap_or(MouseDisplayMode::Normal); } } } @@ -422,9 +404,11 @@ impl FrameInProgress<'_> { let mut moved = false; let mut scaled = false; - if self.gridview.is_dragging_view() { - moved = true; - scaled = true; + if let Some(drag_cmd) = self.gridview.drag_cmd() { + if drag_cmd.is_view_cmd() { + moved = true; + scaled = true; + } } let distance_per_second = if self.state.modifiers.shift() { @@ -467,17 +451,12 @@ impl FrameInProgress<'_> { match self.gridview { GridView::View2D(view2d) => { let move_speed = config.ctrl.keybd_move_speed_2d * speed * self.dpi; - let pan_x = if pan_left { -move_speed } else { 0.0 } + let dx = if pan_left { -move_speed } else { 0.0 } + if pan_right { move_speed } else { 0.0 }; - let pan_y = if pan_south { -move_speed } else { 0.0 } + let dy = if pan_south { -move_speed } else { 0.0 } + if pan_north { move_speed } else { 0.0 }; - if (pan_x, pan_y) != (0.0, 0.0) { - view2d.enqueue(ViewCommand::GoTo2D { - x: Some(r64(pan_x).into()), - y: Some(r64(pan_y).into()), - relative: true, - scaled: true, - }); + if (dx, dy) != (0.0, 0.0) { + view2d.enqueue(Move2D { dx, dy }); moved = true; } @@ -485,32 +464,25 @@ impl FrameInProgress<'_> { let log2_factor = if zoom_in { scale_speed } else { 0.0 } + if zoom_out { -scale_speed } else { 0.0 }; if log2_factor != 0.0 { - let invariant_pos = None; - view2d.enqueue(ViewCommand::Scale { - log2_factor, - invariant_pos, - }); + view2d.enqueue(Cmd::Scale(log2_factor)); scaled = true; } } GridView::View3D(view3d) => { let move_speed = config.ctrl.keybd_move_speed_3d * speed * self.dpi; - let x = if move_left { -move_speed } else { 0.0 } + let dx = if move_left { -move_speed } else { 0.0 } + if move_right { move_speed } else { 0.0 }; - let y = if move_down { -move_speed } else { 0.0 } + let dy = if move_down { -move_speed } else { 0.0 } + if move_up { move_speed } else { 0.0 }; - let z = if move_fwd { -move_speed } else { 0.0 } + let dz = if move_fwd { -move_speed } else { 0.0 } + if move_back { move_speed } else { 0.0 }; - if (x, y, z) != (0.0, 0.0, 0.0) { - view3d.enqueue(ViewCommand::GoTo3D { - x: Some(r64(x).into()), - y: Some(r64(y).into()), - z: Some(r64(z).into()), - yaw: None, - pitch: None, - relative: true, - scaled: true, + if (dx, dy, dz) != (0.0, 0.0, 0.0) { + view3d.enqueue(Move3D { + dx, + dy, + dz, + ..Default::default() }); moved = true; } @@ -522,7 +494,7 @@ impl FrameInProgress<'_> { || (self.gridview.is_3d() && config.ctrl.snap_pos_3d)) { // Snap to the nearest position. - self.gridview.enqueue(ViewCommand::SnapPos); + self.gridview.enqueue(Cmd::SnapPos); } if scaled { self.state.scale_snap_cooldown = Some(Instant::now() + Duration::from_millis(10)); @@ -535,18 +507,16 @@ impl FrameInProgress<'_> { || (self.gridview.is_3d() && config.ctrl.snap_scale_3d)) { // Snap to the nearest power-of-2 scale. - self.gridview.enqueue(ViewCommand::SnapScale { - invariant_pos: None, - }); + self.gridview.enqueue(Cmd::SnapScale); } // Send mouse drag command, if dragging. if self.state.dragging_button.is_some() { - if let Some(pos) = self.state.mouse.pos { - self.gridview.enqueue(Command::ContinueDrag(pos)); + if let Some(mouse_pos) = self.state.mouse.pos { + self.gridview.enqueue(Cmd::ContinueDrag.at(mouse_pos)); } else { - self.gridview.enqueue(Command::StopDrag); + self.gridview.enqueue(Cmd::EndDrag); self.state.dragging_button = None; } } @@ -600,13 +570,3 @@ impl Index for KeysPressed { } } } - -struct DragHandler { - pub continue_command: Box Command>, - pub stop_command: Command, -} -impl fmt::Debug for DragHandler { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "DragHandler {{ stop: {:?} }}", self.stop_command) - } -} diff --git a/ui/src/main.rs b/ui/src/main.rs index fe570440..4efcb123 100644 --- a/ui/src/main.rs +++ b/ui/src/main.rs @@ -28,17 +28,22 @@ mod clipboard_compat; mod colors; mod commands; mod config; +mod direction; mod ext; +mod face; mod gridview; mod gui; mod input; -mod math; mod mouse; +mod plane; mod scale; mod windows; use config::Config; +use direction::{Direction, DIRECTIONS}; +use face::{Face, FACES}; use gui::DISPLAY; +use plane::Plane; use scale::Scale; /// The title of the window (both the OS window, and the main imgui window). @@ -121,3 +126,18 @@ fn make_default_gridview(ndim: usize) -> gridview::GridView { _ => panic!("Invalid number of dimensions passed to make_default_gridview()"), } } + +fn default_colors() -> [palette::Srgba; 256] { + use palette::Srgba; + let mut ret = [Srgba::default(); 256]; + + ret[0] = crate::colors::cells::DEAD; + ret[1] = crate::colors::cells::LIVE; + + for i in 2..256 { + let c = colorous::SPECTRAL.eval_rational(i as usize - 2, 255); + ret[i] = Srgba::new(c.r, c.g, c.b, u8::MAX).into_format(); + } + + ret +} diff --git a/ui/src/mouse.rs b/ui/src/mouse.rs index fc8e44b4..faeecac2 100644 --- a/ui/src/mouse.rs +++ b/ui/src/mouse.rs @@ -2,39 +2,48 @@ use imgui::MouseCursor; use ndcell_core::ndvec::FVec2D; -/// What to display for the mouse cursor. -/// -/// This determines the mouse cursor icon and how/whether to indicate the -/// highlighted cell in the grid. +use crate::commands::DrawMode; +use crate::{Direction, Face}; + +/// The way to display the mouse cursor and the cell it is hovering over. #[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum MouseDisplay { +pub enum MouseDisplayMode { Normal, + + Orbit, Pan, - Draw, + Scale, + + Draw(DrawMode), + Select, - ResizeEW, - ResizeNS, - ResizeNESW, - ResizeNWSE, - ResizeSelectionAbsolute, + ResizeSelectionToCursor, + ResizeSelectionEdge(Direction), + ResizeSelectionFace(Face), + Move, } -impl Default for MouseDisplay { +impl Default for MouseDisplayMode { fn default() -> Self { Self::Normal } } -impl MouseDisplay { +impl MouseDisplayMode { pub fn cursor_icon(self) -> Option { use MouseCursor::*; match self { - Self::Pan => Some(Arrow), // TODO: open palm hand - Self::Draw => Some(Arrow), // TODO: pencil - Self::Select => Some(Arrow), // TODO: crosshairs/plus - Self::ResizeEW => Some(ResizeEW), - Self::ResizeNS => Some(ResizeNS), - Self::ResizeNESW => Some(ResizeNESW), - Self::ResizeNWSE => Some(ResizeNWSE), + Self::Orbit => Some(Arrow), // TODO: some better icon? + Self::Pan => Some(Arrow), // TODO: open palm hand + Self::Scale => Some(ResizeNS), // TODO: some better icon? + Self::Draw(_) => Some(Arrow), // TODO: pencil + Self::Select => Some(Arrow), // TODO: crosshairs/plus + Self::ResizeSelectionEdge(direction) => match direction { + Direction::N | Direction::S => Some(ResizeNS), + Direction::NE | Direction::SW => Some(ResizeNESW), + Direction::E | Direction::W => Some(ResizeEW), + Direction::SE | Direction::NW => Some(ResizeNWSE), + }, + Self::ResizeSelectionFace(_) => Some(Arrow), Self::Move => Some(ResizeAll), _ => Some(Arrow), } @@ -54,5 +63,5 @@ pub struct MouseState { /// /// This determines the mouse cursor icon and how/whether to indicate the /// highlighted cell in the grid. - pub display: MouseDisplay, + pub display_mode: MouseDisplayMode, } diff --git a/ui/src/plane.rs b/ui/src/plane.rs new file mode 100644 index 00000000..521decab --- /dev/null +++ b/ui/src/plane.rs @@ -0,0 +1,8 @@ +use ndcell_core::prelude::*; + +/// Axis-aligned plane in 3D global space. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Plane { + pub axis: Axis, + pub coordinate: FixedPoint, +} diff --git a/ui/src/scale.rs b/ui/src/scale.rs index bebf656d..6156f9d8 100644 --- a/ui/src/scale.rs +++ b/ui/src/scale.rs @@ -68,10 +68,18 @@ impl Scale { } /// Returns the largest `Scale` at which a rectangle of cells fits entirely /// on the target. - pub fn from_fit(cells_size: BigVec2D, target_size: (u32, u32)) -> Self { + pub fn from_fit(cells_size: BigVec, target_size: (u32, u32)) -> Self { let log2_cells_size = - FVec2D::from_fn(|ax| r64(FixedPoint::from(cells_size[ax].clone()).log2())); - let NdVec([log2_cells_w, log2_cells_h]) = log2_cells_size; + FVec::::from_fn(|ax| r64(FixedPoint::from(cells_size[ax].clone()).log2())); + + let (log2_cells_w, log2_cells_h) = if D::NDIM == 2 { + // Unpack vector into two values. + (log2_cells_size[Axis::X], log2_cells_size[Axis::Y]) + } else { + // Clone the same value. + let max = *log2_cells_size.max_component(); + (max, max) + }; let log2_target_w: R64 = R64::from_u32(target_size.0).unwrap().log2(); let log2_target_h: R64 = R64::from_u32(target_size.1).unwrap().log2(); diff --git a/ui/src/windows/error_popup.rs b/ui/src/windows/error_popup.rs new file mode 100644 index 00000000..b8d5f331 --- /dev/null +++ b/ui/src/windows/error_popup.rs @@ -0,0 +1,81 @@ +use anyhow::Result; +use imgui::{im_str, ImString, Ui}; +use parking_lot::Mutex; + +use crate::gridview::GridView; + +lazy_static! { + static ref LAST_ERROR: Mutex> = Mutex::new(None); + static ref CLIPBOARD_ERROR: Mutex = Mutex::new(false); + static ref ERROR_POPUP_TITLE: &'static imgui::ImStr = im_str!("An internal error has occurred"); +} + +pub fn show_error_popup_on_error( + ui: &Ui<'_>, + gridview: &GridView, + gridview_frame_result: Result<()>, +) { + let mut last_error = LAST_ERROR.lock(); + let mut clipboard_error = CLIPBOARD_ERROR.lock(); + + if let Err(e) = gridview_frame_result { + if last_error.is_none() { + ui.open_popup(*ERROR_POPUP_TITLE); + *last_error = Some(format!("Error occurred while: {:?}", e)); + } + } + + let error_string = last_error.clone().unwrap_or_default(); + + unsafe { + // Workaround for https://github.com/imgui-rs/imgui-rs/issues/201 + imgui::sys::igSetNextWindowSize( + imgui::sys::ImVec2::new(500.0, 0.0), + imgui::Condition::Always as i32, + ); + } + + let mut stayed_open = true; + ui.popup_modal(*ERROR_POPUP_TITLE) + .opened(&mut stayed_open) + .resizable(false) + .build(|| { + ui.text_wrapped(im_str!("Yikes! That wasn't supposed to happen.")); + ui.text(""); + if *clipboard_error { + ui.text_wrapped(im_str!("Oh geez, another error just happened when you clicked that button. You're on your own now.")); + } else { + ui.text_wrapped(im_str!("Don't worry, your work isn't lost. The 'OK' button should take you right back where you were. But in case the error keeps happening, click the button below to copy everything to the clipboard:")); + if ui.button(im_str!("Copy CA contents to clipboard"), [0.0, 0.0]) { + let fmt = ndcell_core::io::CaFormat::Macrocell; + if let Ok(s) = gridview.export(fmt) { + if crate::clipboard_compat::clipboard_set(s).is_err() { + *clipboard_error = true; + } + } else { + *clipboard_error = true; + } + } + } + ui.text(""); + ui.text_wrapped(im_str!("Please report this to the developer, along with the exception info below:")); + ui.input_text_multiline(im_str!(""), &mut ImString::new(&error_string), [0.0, 0.0]) + .read_only(true) + .build(); + if ui.button(im_str!("Copy exception info"), [0.0, 0.0]) { + if crate::clipboard_compat::clipboard_set(error_string).is_err() { + *clipboard_error = true; + } + } + ui.text(""); + + if ui.button(im_str!("OK"), [0.0, 0.0]) { + ui.close_current_popup(); + } + }); + + if !stayed_open { + *last_error = None; + *clipboard_error = false; + } +} diff --git a/ui/src/windows/mod.rs b/ui/src/windows/mod.rs index b8c2e02f..9f5ef321 100644 --- a/ui/src/windows/mod.rs +++ b/ui/src/windows/mod.rs @@ -5,14 +5,17 @@ use ndcell_core::prelude::*; #[cfg(debug_assertions)] mod debug; +pub mod error_popup; +mod setup; mod simulation; -use crate::commands::Command; +use crate::commands::Cmd; use crate::gridview::*; use crate::mouse::MouseState; use crate::CONFIG; #[cfg(debug_assertions)] use debug::DebugWindow; +use setup::SetupWindow; use simulation::SimulationWindow; const RED: [f32; 4] = [1.0, 0.0, 0.0, 1.0]; @@ -28,9 +31,10 @@ pub struct BuildParams<'a> { #[derive(Debug, Default)] pub struct MainWindow { - simulation: SimulationWindow, #[cfg(debug_assertions)] debug: DebugWindow, + setup: SetupWindow, + simulation: SimulationWindow, } impl MainWindow { /// Builds the window. @@ -45,6 +49,43 @@ impl MainWindow { let config = CONFIG.lock(); ui.text(format!("NDCell v{}", env!("CARGO_PKG_VERSION"))); + ui.text(""); + + let width = ui.window_content_region_width(); + let button_width = (width - 10.0) / 2.0; + if ui.button(im_str!("Load file"), [button_width, 40.0]) { + if let Ok(response) = nfd2::open_file_dialog(Some("rle,mc"), None) { + if let nfd2::Response::Okay(path) = response { + let rule = gridview.rule(); + if let Ok(s) = std::fs::read_to_string(path) { + if let Ok(automaton) = + ndcell_core::io::import_automaton_from_string(&s, rule) + { + match automaton.unwrap() { + Automaton::Automaton2D(a) => **gridview = a.into(), + Automaton::Automaton3D(a) => **gridview = a.into(), + _ => (), + } + } + } + } + } + } + ui.same_line(button_width + 18.0); + if ui.button(im_str!("Save file"), [button_width, 40.0]) { + if let Ok(response) = nfd2::open_save_dialog(Some("rle;mc"), None) { + if let nfd2::Response::Okay(path) = response { + let ca_format = match path.extension() { + Some(ext) if ext == "mc" => CaFormat::Macrocell, + _ => CaFormat::Rle, + }; + if let Ok(s) = gridview.export(ca_format) { + let _ = std::fs::write(path, s); + } + } + } + } + ui.text(""); let fps = ui.io().framerate.ceil() as usize; ui.text_colored(fps_color(fps), format!("Framerate = {} FPS", fps)); @@ -123,17 +164,9 @@ impl MainWindow { } ui.text(format!("Pitch = {:.2?}°", vp.pitch().0)); ui.text(format!("Yaw = {:.2?}°", vp.yaw().0)); - if let Some(hit) = mouse - .pos - .and_then(|pixel| view3d.screen_pos(pixel).raycast()) + if let Some(hit) = mouse.pos.and_then(|pixel| view3d.screen_pos(pixel).raycast) { - let (axis, sign) = hit.face; - let sign = match sign { - Sign::Minus => "-", - Sign::NoSign => "", - Sign::Plus => "+", - }; - ui.text(format!("Cursor: {} {}{:?}", hit.cell, sign, axis)); + ui.text(format!("Cursor: {} {}", hit.cell, hit.face)); } else { ui.text(""); } @@ -146,10 +179,10 @@ impl MainWindow { config.sim.max_memory.div_ceil(&MEBIBYTE), )); if ui.button( - im_str!("Trigger garbage collection"), + im_str!("Clear cache"), [ui.window_content_region_width(), 30.0], ) { - gridview.enqueue(Command::GarbageCollect) + gridview.enqueue(Cmd::ClearCache); } ui.text(""); ui.text(format!( @@ -157,26 +190,25 @@ impl MainWindow { gridview.selected_cell_state(), )); ui.text(""); + ui.checkbox(im_str!("Setup"), &mut self.setup.is_visible); ui.checkbox(im_str!("Simulation"), &mut self.simulation.is_visible); #[cfg(debug_assertions)] ui.checkbox(im_str!("Debug values"), &mut self.debug.is_visible); }); - self.simulation.build(params); #[cfg(debug_assertions)] self.debug.build(params); + self.setup.build(params); + self.simulation.build(params); } } fn fps_color(fps: usize) -> [f32; 4] { if fps >= 58 { - // Green - [0.0, 1.0, 0.0, 1.0] + GREEN } else if fps >= 29 { - // Yellow - [1.0, 1.0, 0.0, 1.0] + YELLOW } else { - // Red - [1.0, 0.0, 0.0, 1.0] + RED } } diff --git a/ui/src/windows/setup.rs b/ui/src/windows/setup.rs new file mode 100644 index 00000000..d6a1ed40 --- /dev/null +++ b/ui/src/windows/setup.rs @@ -0,0 +1,160 @@ +use anyhow::{anyhow, bail, Context, Result}; +use imgui::*; +use palette::Srgba; +use std::convert::TryInto; +use std::sync::Arc; + +use ndcell_core::traits::*; + +use crate::gridview::{GridView, GridView2D, GridView3D}; +use crate::windows::BuildParams; +use crate::CONFIG; + +#[derive(Debug, Default)] +pub struct SetupWindow { + pub is_visible: bool, + pub error_message: Option, +} +impl SetupWindow { + fn error(&mut self, ui: &Ui<'_>, error: anyhow::Error) { + self.error_message = Some(format!("{:?}", error)); + ui.open_popup(im_str!("Error")); + } + + fn load_rule_from_clipboard(gridview: &mut GridView) -> Result<()> { + let source_code = Arc::new(crate::clipboard_compat::clipboard_get()?); + let rule = ndcell_lang::compile_blocking(Arc::clone(&source_code), None) + .map_err(|e| e.with_source(&source_code)) + .context("Compiling rule")?; + println!("{:?}", rule.rule_meta()); + if rule.rule_meta().ndim != gridview.ndim() { + *gridview = match rule.rule_meta().ndim { + 2 => GridView2D::default().into(), + 3 => GridView3D::default().into(), + d => bail!("Cannot display {}D simulation", d), + } + } + match gridview { + GridView::View2D(gv2d) => gv2d.automaton.rule = rule.into_arc(), + GridView::View3D(gv3d) => gv3d.automaton.rule = rule.into_arc(), + } + + Ok(()) + } + fn load_cell_pattern_from_clipboard(gridview: &mut GridView) -> Result<()> { + use ndcell_core::io::import_automaton_from_string; + let s = crate::clipboard_compat::clipboard_get()?; + + match gridview { + GridView::View2D(gv2d) => { + gv2d.automaton = import_automaton_from_string(&s, Arc::clone(&gv2d.automaton.rule)) + .map_err(|es| anyhow!("{}", itertools::join(es, "\n"))) + .context("Importing cell pattern")? + .unwrap() + .try_into() + .unwrap(); + } + GridView::View3D(gv3d) => { + gv3d.automaton = import_automaton_from_string(&s, Arc::clone(&gv3d.automaton.rule)) + .map_err(|es| anyhow!("{}", itertools::join(es, "\n"))) + .context("Importing cell pattern")? + .unwrap() + .try_into() + .unwrap(); + } + } + + Ok(()) + } + fn load_colors_from_clipboard() -> Result<()> { + let mut cfg = CONFIG.lock(); + let colors = &mut cfg.gfx.cell_colors; + + let s = crate::clipboard_compat::clipboard_get()?; + let mut i = 1; + for line in s.lines() { + if !line.is_empty() { + let c: css_color_parser::Color = line + .parse() + .context(format!("Unable to parse color: {:?}", line))?; + colors[i] = Srgba::new( + c.r as f32 / 255.0, + c.g as f32 / 255.0, + c.b as f32 / 255.0, + c.a, + ); + i += 1; + if i > colors.len() { + break; + } + } + } + crate::gridview::render::invalidate_gl_ndtree_cache(); + + Ok(()) + } + + /// Builds the window. + pub fn build(&mut self, params: &mut BuildParams<'_>) { + let BuildParams { ui, gridview, .. } = params; + + if self.is_visible { + Window::new(im_str!("Setup")) + .size([300.0, 0.0], Condition::FirstUseEver) + .flags(WindowFlags::NO_RESIZE) + .build(&ui, || { + if ui.button( + im_str!("Load rule from clipboard"), + [ui.window_content_region_width(), 60.0], + ) { + if let Err(e) = Self::load_rule_from_clipboard(gridview) { + self.error(ui, e); + } + } + + if ui.button( + im_str!("Load cell pattern from clipboard"), + [ui.window_content_region_width(), 60.0], + ) { + if let Err(e) = Self::load_cell_pattern_from_clipboard(gridview) { + self.error(ui, e); + } + } + + if ui.button( + im_str!( + "Load colors from clipboard\n(one color per line, CSS color format)" + ), + [ui.window_content_region_width(), 60.0], + ) { + if let Err(e) = Self::load_colors_from_clipboard() { + self.error(ui, e); + } + } + + if ui.button( + im_str!("Reset colors"), + [ui.window_content_region_width(), 30.0], + ) { + CONFIG.lock().gfx.cell_colors = crate::default_colors(); + } + + ui.popup_modal(im_str!("Error")) + .flags(WindowFlags::NO_RESIZE | WindowFlags::NO_MOVE) + .build(|| { + if let Some(e) = &self.error_message { + ui.text(e); + ui.text(""); + if ui.button(im_str!("OK"), [ui.window_content_region_width(), 0.0]) + { + self.error_message = None; + ui.close_current_popup(); + } + } else { + ui.close_current_popup(); + } + }); + }) + } + } +} diff --git a/ui/src/windows/simulation.rs b/ui/src/windows/simulation.rs index 71191bc3..154c2cb3 100644 --- a/ui/src/windows/simulation.rs +++ b/ui/src/windows/simulation.rs @@ -28,7 +28,7 @@ impl SimulationWindow { width = 200.0; } if ui.button(im_str!("Step 1 generation"), [width, 40.0]) { - gridview.enqueue(SimCommand::Step(1.into())); + gridview.enqueue(Cmd::Step(1)); } ui.spacing(); ui.spacing(); @@ -52,7 +52,7 @@ impl SimulationWindow { &ImString::new(format!("Step {} generations", config.sim.step_size)), [width, 40.0], ) { - gridview.enqueue(SimCommand::StepStepSize); + gridview.enqueue(Cmd::StepStepSize); } ui.spacing(); ui.spacing(); @@ -65,7 +65,7 @@ impl SimulationWindow { }, [width, 60.0], ) { - gridview.enqueue(SimCommand::ToggleRunning); + gridview.enqueue(Cmd::ToggleRunning); } } ui.spacing(); @@ -94,18 +94,18 @@ impl SimulationWindow { ui.separator(); ui.spacing(); ui.spacing(); - let button_width = (width - 20.0) / 2.0; + let button_width = (width - 10.0) / 2.0; if ui.button(im_str!("Undo"), [button_width, 60.0]) { - gridview.enqueue(HistoryCommand::Undo); + gridview.enqueue(Cmd::Undo); } - ui.same_line(button_width + 20.0); + ui.same_line(button_width + 18.0); if ui.button(im_str!("Redo"), [button_width, 60.0]) { - gridview.enqueue(HistoryCommand::Redo); + gridview.enqueue(Cmd::Redo); } ui.spacing(); ui.spacing(); if ui.button(im_str!("Reset"), [width, 40.0]) { - gridview.enqueue(HistoryCommand::UndoTo(0.into())); + gridview.enqueue(Cmd::Reset); } }) }