diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index adc09b1..96e6c1a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -44,6 +44,10 @@ jobs: run: cargo build --verbose --all - name: Run tests run: cargo nextest run + - name: Install wasm-pack + uses: taiki-e/install-action@wasm-pack + - name: Build WASM + run: wasm-pack build --target web crates/viewshed-reconstructor e2e: name: End to end tests diff --git a/Cargo.lock b/Cargo.lock index d0dc4a3..ba0dacd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -261,6 +261,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "console_log" version = "1.0.0" @@ -414,6 +424,30 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "gdal" version = "0.19.0" @@ -755,11 +789,12 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -1239,6 +1274,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "smallvec" version = "1.15.1" @@ -1364,6 +1405,7 @@ dependencies = [ "tracing", "tracing-subscriber", "tvs-lib", + "viewshed-reconstructor", ] [[package]] @@ -1484,6 +1526,19 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "viewshed-reconstructor" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "geo", + "js-sys", + "tracing", + "tracing-subscriber", + "tvs-lib", + "wasm-bindgen", +] + [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -1495,9 +1550,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -1508,9 +1563,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1518,9 +1573,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", @@ -1531,18 +1586,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 0ce23b3..1465e06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "crates/total-viewsheds", "crates/tvs-lib", + "crates/viewshed-reconstructor", ] [workspace.dependencies] @@ -10,6 +11,8 @@ color-eyre = "0.6.5" geo = "0.31.0" serde = "1.0.219" serde_json = "1.0.143" +tracing = { version = "0.1.41" } +tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } # Canonical lints for whole crate # diff --git a/crates/total-viewsheds/Cargo.toml b/crates/total-viewsheds/Cargo.toml index a3b8c35..28bd1f8 100644 --- a/crates/total-viewsheds/Cargo.toml +++ b/crates/total-viewsheds/Cargo.toml @@ -18,9 +18,10 @@ memchr = "2.8.0" serde_json.workspace = true rusqlite = { version = "0.38.0", features = ["bundled"] } rayon = "1.12.0" -tracing = { version = "0.1.41" } -tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } +tracing.workspace = true +tracing-subscriber.workspace = true tvs-lib = { path = "../tvs-lib", version = "0.1.0" } +viewshed-reconstructor = { path = "../viewshed-reconstructor", version = "0.1.0" } [dev-dependencies] tempfile = "3.20.0" diff --git a/crates/total-viewsheds/src/main.rs b/crates/total-viewsheds/src/main.rs index af224ba..8f281b4 100644 --- a/crates/total-viewsheds/src/main.rs +++ b/crates/total-viewsheds/src/main.rs @@ -70,18 +70,9 @@ mod storage { /// Various ways to output data. mod output { mod ascii; - mod bresenham; pub(crate) mod png; pub(crate) mod tiff; - - /// Load, parse and reconstruct euclidean polygon viewsheds from their raw polar segments. - pub(crate) mod viewsheds { - mod growable_polygon; - mod joiner; - mod segment_polygon; - mod vertices; - pub(crate) mod viewshed; - } + pub(crate) mod viewshed; } fn main() -> Result<()> { @@ -98,11 +89,11 @@ fn main() -> Result<()> { geo::coord! {x: f64::from(coordinate.0), y: f64::from(coordinate.1)}, ); - let (_, viewshed) = crate::output::viewsheds::viewshed::Viewshed::reconstruct( + let (_, viewshed) = crate::output::viewshed::Viewshed::reconstruct( viewshed_config.db_path.clone(), geo_coord, )?; - crate::output::viewsheds::viewshed::Viewshed::save( + crate::output::viewshed::Viewshed::save( viewshed, &viewshed_config.output_dir, geo_coord, diff --git a/crates/total-viewsheds/src/output/ascii.rs b/crates/total-viewsheds/src/output/ascii.rs index d451f08..4f5075b 100644 --- a/crates/total-viewsheds/src/output/ascii.rs +++ b/crates/total-viewsheds/src/output/ascii.rs @@ -1,12 +1,6 @@ -//! Output viewsheds as ASCII. Most likely useful for tests. - -#![cfg(test)] -#![expect(clippy::indexing_slicing, reason = "This code is mostly for tests")] - -use geo::CoordsIter as _; - -use crate::output::viewsheds::viewshed::Viewshed; +//! Convert a viewshed to pure text. For testing. +#[cfg(test)] pub(crate) fn make_viewshed( elevations: &[i16], viewshed_pov: geo::Coord, @@ -24,8 +18,8 @@ pub(crate) fn make_viewshed( crate::run::test::compute(&mut dem, config.clone()); let (pov_coord, mut viewshed) = - Viewshed::reconstruct(config.viewsheds_db_path, coord_lonlat).unwrap(); - let viewsheder = crate::output::viewsheds::viewshed::Viewshed { + super::viewshed::Viewshed::reconstruct(config.viewsheds_db_path, coord_lonlat).unwrap(); + let viewsheder = crate::output::viewshed::Viewshed { dem: &dem, pov_coord, }; @@ -34,9 +28,9 @@ pub(crate) fn make_viewshed( polygons.exterior_mut(|line| { for coordinate in line.coords_mut() { *coordinate = viewsheder - .convert_viewshed_coord_to_dem_coord( - crate::output::viewsheds::viewshed::Coordinate(*coordinate), - ) + .convert_viewshed_coord_to_dem_coord(crate::output::viewshed::Coordinate( + *coordinate, + )) .unwrap(); } }); @@ -45,165 +39,14 @@ pub(crate) fn make_viewshed( for interiror_line in interior { for coordinate in interiror_line.coords_mut() { *coordinate = viewsheder - .convert_viewshed_coord_to_dem_coord( - crate::output::viewsheds::viewshed::Coordinate(*coordinate), - ) + .convert_viewshed_coord_to_dem_coord(crate::output::viewshed::Coordinate( + *coordinate, + )) .unwrap(); } } }); } - rasterise_multi_polygon(&viewshed, dem.width) -} - -pub(crate) fn rasterise_multi_polygon( - multi_polygon: &geo::MultiPolygon, - width: u32, -) -> Vec { - let scale = std::env::var("ASCII_TEST_SIZE") - .unwrap_or_else(|_| "2".to_owned()) - .parse::() - .expect("ASCII_TEST_SIZE env not a valid integer"); - let scale_f64 = f64::from(scale); - let raster_width = usize::try_from(width * scale).unwrap(); - - let mut raster: Vec> = Vec::new(); - for _ in 0..width * scale { - let raster_line = vec![false; raster_width]; - raster.push(raster_line); - } - - for polygon in multi_polygon.iter() { - let mut maybe_exterior_start = None; - for coordinate in polygon.exterior_coords_iter() { - if maybe_exterior_start.is_none() { - maybe_exterior_start = Some(coordinate); - continue; - } - - let from = maybe_exterior_start.unwrap(); - #[expect( - clippy::as_conversions, - clippy::cast_possible_truncation, - reason = "Gotta rasterise" - )] - let rasteriser = crate::output::bresenham::Bresenham::new( - super::bresenham::RasterCoord { - x: (from.x * scale_f64).round() as i32, - y: (from.y * scale_f64).round() as i32, - }, - super::bresenham::RasterCoord { - x: (coordinate.x * scale_f64).round() as i32, - y: (coordinate.y * scale_f64).round() as i32, - }, - ); - for coord in rasteriser { - if coord.x >= 0i32 && coord.y >= 0i32 { - let x = usize::try_from(coord.x).unwrap(); - let y = usize::try_from(coord.y).unwrap(); - raster[y][x] = true; - } - } - maybe_exterior_start = Some(coordinate); - } - - for interiors in polygon.interiors() { - let mut maybe_interior_start = None; - for coordinate in interiors { - if maybe_interior_start.is_none() { - maybe_interior_start = Some(*coordinate); - continue; - } - - let from = maybe_interior_start.unwrap(); - #[expect( - clippy::as_conversions, - clippy::cast_possible_truncation, - reason = "Gotta rasterise" - )] - let rasteriser = crate::output::bresenham::Bresenham::new( - super::bresenham::RasterCoord { - x: (from.x * scale_f64).round() as i32, - y: (from.y * scale_f64).round() as i32, - }, - super::bresenham::RasterCoord { - x: (coordinate.x * scale_f64).round() as i32, - y: (coordinate.y * scale_f64).round() as i32, - }, - ); - for coord in rasteriser { - if coord.x >= 0i32 && coord.y >= 0i32 { - let x = usize::try_from(coord.x).unwrap(); - let y = usize::try_from(coord.y).unwrap(); - raster[y][x] = true; - } - } - maybe_interior_start = Some(*coordinate); - } - } - } - - let mut ascii = Vec::new(); - let row_count = raster_width.div_euclid(2); - #[expect(clippy::needless_range_loop, reason = "clippy is wrong")] - for row in 0..row_count { - let mut viewshed_line = String::new(); - for x in 0..raster_width { - let y = row * 2; - let upper = raster[y][x]; - let lower = raster[y + 1][x]; - let character = match (upper, lower) { - (true, true) => ' ', - (false, true) => '▀', - (true, false) => '▄', - (false, false) => '█', - }; - viewshed_line.push(character); - } - ascii.push(viewshed_line); - } - - ascii -} - -pub(crate) fn rasterise(mut multi_polygon: geo::MultiPolygon) -> Vec { - let width = 12u32; - let centre = f64::from(width.div_euclid(2)); - for polygons in multi_polygon.iter_mut() { - polygons.exterior_mut(|line| { - for coordinate in line.coords_mut() { - *coordinate = geo::Coord { - x: coordinate.x + centre, - y: coordinate.y + centre, - }; - } - }); - - polygons.interiors_mut(|interior| { - for interiror_line in interior { - for coordinate in interiror_line.coords_mut() { - *coordinate = geo::Coord { - x: coordinate.x + centre, - y: coordinate.y + centre, - }; - } - } - }); - } - crate::output::ascii::rasterise_multi_polygon(&multi_polygon, width) -} - -#[expect(clippy::print_stderr, reason = "This is for tests")] -pub(crate) fn assert_rasterised(actual: &[String], expected: &[&str]) { - if actual != expected { - eprintln!("Actual:"); - eprint!("{}", actual.join("\n")); - eprintln!(); - eprintln!(); - eprintln!("Expected:"); - eprint!("{}", expected.join("\n")); - eprintln!(); - panic!("Viewsheds do not match"); - } + tvs_lib::ascii::rasterise_multi_polygon_geo(&viewshed, dem.width) } diff --git a/crates/total-viewsheds/src/output/viewsheds/viewshed.rs b/crates/total-viewsheds/src/output/viewshed.rs similarity index 89% rename from crates/total-viewsheds/src/output/viewsheds/viewshed.rs rename to crates/total-viewsheds/src/output/viewshed.rs index 9c38da3..0a40077 100644 --- a/crates/total-viewsheds/src/output/viewsheds/viewshed.rs +++ b/crates/total-viewsheds/src/output/viewshed.rs @@ -6,6 +6,7 @@ use color_eyre::eyre::Result; /// point of view. The other option would be a metric projection with an anchor in the DEM centre, /// but metric projections are not globally correct. So reprojecting to the _viewshed's_ centre /// just gives us that little bit more accuracy, especially for larger DEMs. +#[cfg(test)] #[derive(Debug, Clone, Copy, PartialEq)] pub(crate) struct Coordinate(pub geo::Coord); @@ -85,15 +86,59 @@ impl Viewshed<'_> { color_eyre::eyre::bail!("Point of view ({:?}) is not calculable", viewshed.pov_coord); } - let multi_polygon = crate::output::viewsheds::joiner::build_viewshed_polygon( - &segments, - viewshed.dem.scale, - )?; + let multi_polygon = Self::build_viewshed_polygon(&segments, viewshed.dem.scale); tracing::info!("Viewshed reconstructed in {:?}.", start.elapsed()); Ok((pov_dem_coord, multi_polygon)) } + /// Build a viewshed of euclidean polygons from polar segments. + pub(crate) fn build_viewshed_polygon( + data: &[Vec], + dem_scale: f32, + ) -> geo::MultiPolygon { + let raw_polygons = viewshed_reconstructor::joiner::Joiner::join(data, dem_scale); + let mut geo_polygons = Vec::new(); + for raw_polygon in raw_polygons { + let geo_polygon = Self::to_geo_polygon(&raw_polygon.to_polygon()); + tracing::trace!("Final viewshed, adding polygon: {geo_polygon:?}"); + geo_polygons.push(geo_polygon); + } + + geo::MultiPolygon::new(geo_polygons) + } + + /// Convert the polygon to the `geo` crate's representation. Ready for exporting to `GeoJSON`. + pub(crate) fn to_geo_polygon( + polygon: &viewshed_reconstructor::polygon::Polygon, + ) -> geo::Polygon { + let holes: Vec = polygon + .interior + .iter() + .map(|hole| { + geo::LineString( + hole.iter() + .map(|coordinate| geo::Coord { + x: coordinate.x, + y: coordinate.y, + }) + .collect::>(), + ) + }) + .collect(); + + let vertices: Vec = polygon + .exterior + .iter() + .map(|coordinate| geo::Coord { + x: coordinate.x, + y: coordinate.y, + }) + .collect(); + + geo::Polygon::new(geo::LineString(vertices), holes) + } + #[expect( clippy::panic, reason = "The closures expect () so I don't think there's any other way?" @@ -189,7 +234,7 @@ impl Viewshed<'_> { #[cfg(test)] mod test { - use crate::output::ascii::assert_rasterised; + use tvs_lib::ascii::assert_rasterised; const SUMMIT_VIEWSHED: [&str; 12] = [ "████████████████████████", diff --git a/crates/total-viewsheds/src/output/viewsheds/joiner.rs b/crates/total-viewsheds/src/output/viewsheds/joiner.rs deleted file mode 100644 index 1052a7b..0000000 --- a/crates/total-viewsheds/src/output/viewsheds/joiner.rs +++ /dev/null @@ -1,71 +0,0 @@ -//! Join segments and polygons to existing growable polygons. - -mod last; -mod normal; - -use color_eyre::Result; - -/// Keeps track of active and completed polygons within a viewshed. -#[derive(Default)] -struct Joiner { - /// Polygons that don't intersect with the current angle. They don't need to be checked against - /// new segments. - completed: Vec, - /// Polygons that intersect with the current angle. They must be checked to see if any of their - /// openings touch any of the segments of the current angle. - active: Vec, -} - -impl Joiner { - /// Join segments into a collection of polygons. - fn join( - data: &[Vec], - dem_scale: f32, - ) -> Result> { - #[expect( - clippy::as_conversions, - clippy::cast_precision_loss, - reason = "The angle count should never strain the f32 mantissa" - )] - let angle_count = data.len() as f32; - let angle_scale = angle_count / 360.0; - - let mut joiner = Self { - completed: Vec::new(), - active: Vec::new(), - }; - - for (anglish, segments) in data.iter().enumerate() { - #[expect( - clippy::as_conversions, - clippy::cast_precision_loss, - reason = "The angle count should never strain the f32 mantissa" - )] - let angle = anglish as f32 / angle_scale; - - joiner.build_angle(angle, angle_scale, segments, dem_scale)?; - } - - joiner.build_final_angle()?; - joiner.move_all_active_polygons_to_completed(); - - Ok(joiner.completed) - } -} - -/// Build a viewshed of euclidean polygons from polar segments. -pub(crate) fn build_viewshed_polygon( - data: &[Vec], - dem_scale: f32, -) -> Result { - let raw_polygons = Joiner::join(data, dem_scale)?; - let mut geo_polygons = Vec::new(); - for mut raw_polygon in raw_polygons { - raw_polygon.dedup_vertices_ignore_openings(); - let polygon = raw_polygon.to_geo_polygon(); - tracing::trace!("Final viewshed, adding polygon: {polygon:?}"); - geo_polygons.push(polygon); - } - - Ok(geo::MultiPolygon::new(geo_polygons)) -} diff --git a/crates/total-viewsheds/src/storage/db.rs b/crates/total-viewsheds/src/storage/db.rs index 09f7794..8d52563 100644 --- a/crates/total-viewsheds/src/storage/db.rs +++ b/crates/total-viewsheds/src/storage/db.rs @@ -65,7 +65,7 @@ impl DB { pub(crate) fn load_segments( &self, id: &ID, - ) -> Result<(Vec>, i64)> { + ) -> Result<(Vec>, i64)> { tracing::debug!( "Loading polar segments for {id:?} from {:?}...", self.connection.path() @@ -118,12 +118,14 @@ impl DB { } /// Convert blob to `Segment`s. - fn bytes_to_segments(bytes: &[u8]) -> Result> { + fn bytes_to_segments(bytes: &[u8]) -> Result> { let mut out = Vec::with_capacity(bytes.len().div_euclid(4)); for chunk in bytes.chunks(4) { let array: [u8; 4] = chunk.try_into()?; #[expect(clippy::big_endian_bytes, reason = "That's how we save them in the DB")] - out.push(super::segments::Segment(u32::from_be_bytes(array))); + out.push(viewshed_reconstructor::segment::Segment( + u32::from_be_bytes(array), + )); } Ok(out) } diff --git a/crates/total-viewsheds/src/storage/segments.rs b/crates/total-viewsheds/src/storage/segments.rs index ed4a8d8..e795cfd 100644 --- a/crates/total-viewsheds/src/storage/segments.rs +++ b/crates/total-viewsheds/src/storage/segments.rs @@ -1,40 +1,6 @@ //! A "segment" is a single area of visibility for a given angle on a viewshed. There may be many //! segments per angle. -/// `Segment` is the rho portion of a line segment in polar coordinates -/// as (`rho`: u16, `delta_rho`: u16) which are packed into a single u32 for storage -#[derive(Clone, Default)] -pub(crate) struct Segment(pub u32); - -impl Segment { - /// `new` creates a `Segment` the segment's start point and the distance - pub(crate) fn new(start: u16, distance: u16) -> Self { - // pack start/distsance into a u32 in the format of (start|distance) - let wide_start: u32 = start.into(); - let wide_distance: u32 = distance.into(); - Self((wide_start << 16) | wide_distance) - } - - /// `start` returns the starting point of the `Segment` - #[expect( - clippy::as_conversions, - reason = "the top 16 bits are guaranteed to be 0" - )] - pub(crate) const fn start(&self) -> u16 { - (self.0 >> 16) as u16 - } - - /// `distance` returns the distance the `Segment` takes - #[expect( - clippy::as_conversions, - clippy::cast_possible_truncation, - reason = "the top 16 bits are guaranteed to be 0" - )] - pub(crate) const fn distance(&self) -> u16 { - self.0 as u16 - } -} - /// `PolarSegments` holds the degree of a line of sight and the list /// of visible `Segments` which is constucted through a Run Length /// Encoding algorithm. @@ -48,7 +14,7 @@ pub(crate) struct PolarSegments { pub degree: u16, /// `visible_segments` is a list of segments visible for a given /// angle and tvs id - pub visible_segments: Vec, + pub visible_segments: Vec, } impl PolarSegments { @@ -63,7 +29,8 @@ impl PolarSegments { reason = "we want to panic if out of indexes are oob" )] pub(crate) fn from_bools(degree: u16, bitmap: &[bool]) -> Self { - let mut visible_segments: Vec = Vec::with_capacity(1); + let mut visible_segments: Vec = + Vec::with_capacity(1); let char_slice: &[u8] = bytemuck::cast_slice(bitmap); @@ -72,7 +39,7 @@ impl PolarSegments { let first_zero_index = memchr::memchr(0, &char_slice[cur_index..]).unwrap_or(char_slice.len() - cur_index); - visible_segments.push(Segment::new( + visible_segments.push(viewshed_reconstructor::segment::Segment::new( u16::try_from(cur_index).expect("cur_index overflowed"), u16::try_from(first_zero_index).expect("first_zero_index overflowed"), )); diff --git a/crates/total-viewsheds/src/tests/fixtures.rs b/crates/total-viewsheds/src/tests/fixtures.rs index e554e62..022898e 100644 --- a/crates/total-viewsheds/src/tests/fixtures.rs +++ b/crates/total-viewsheds/src/tests/fixtures.rs @@ -5,36 +5,6 @@ use color_eyre::eyre::Result; /// The nodata value from NASA's SRTM data. pub(crate) const NODATA: f32 = -32768.0; -// This is a little map to help orient yourself when looking at the results: -// -// 0 1 2 3 4 5 6 7 8 -// 9 10 11 12 13 14 15 16 17 -// 18 19 20 21 22 23 24 25 26 -// 27 28 29 30 31 32 33 34 35 -// 36 37 38 39 40 41 42 43 44 -// 45 46 47 48 49 50 51 52 53 -// 54 55 56 57 58 59 60 61 62 -// 63 64 65 66 67 68 69 70 71 -// 72 73 74 75 76 77 78 79 80 - -#[rustfmt::skip] -#[inline] -#[must_use] -/// A DEM tile with a symmetrical single peak. -pub(crate) fn single_peak_dem() -> Vec { - vec![ - 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 1, 1, 1, 1, 1, 1, 1, 0, - 0, 1, 3, 3, 3, 3, 3, 1, 0, - 0, 1, 3, 6, 6, 6, 3, 1, 0, - 0, 1, 3, 6, 9, 6, 3, 1, 0, - 0, 1, 3, 6, 6, 6, 3, 1, 0, - 0, 1, 3, 3, 3, 3, 3, 1, 0, - 0, 1, 1, 1, 1, 1, 1, 1, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0 - ] -} - // This is a little map to help orient yourself when looking at the results: // // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 diff --git a/crates/tvs-lib/src/ascii.rs b/crates/tvs-lib/src/ascii.rs new file mode 100644 index 0000000..db7f945 --- /dev/null +++ b/crates/tvs-lib/src/ascii.rs @@ -0,0 +1,137 @@ +//! Output viewsheds as ASCII. Most likely useful for tests. + +#![expect( + clippy::indexing_slicing, + clippy::expect_used, + clippy::unwrap_used, + clippy::panic, + reason = "This code is mostly for tests" +)] + +use geo::CoordsIter as _; + +/// # Panics +#[inline] +#[must_use] +pub fn rasterise_multi_polygon_geo(multi_polygon: &geo::MultiPolygon, width: u32) -> Vec { + let scale = std::env::var("ASCII_TEST_SIZE") + .unwrap_or_else(|_| "2".to_owned()) + .parse::() + .expect("ASCII_TEST_SIZE env not a valid integer"); + let scale_f64 = f64::from(scale); + let raster_width = usize::try_from(width * scale).unwrap(); + + let mut raster: Vec> = Vec::new(); + for _ in 0..width * scale { + let raster_line = vec![false; raster_width]; + raster.push(raster_line); + } + + for polygon in multi_polygon.iter() { + let mut maybe_exterior_start = None; + for coordinate in polygon.exterior_coords_iter() { + if maybe_exterior_start.is_none() { + maybe_exterior_start = Some(coordinate); + continue; + } + + let from = maybe_exterior_start.unwrap(); + #[expect( + clippy::as_conversions, + clippy::cast_possible_truncation, + reason = "Gotta rasterise" + )] + let rasteriser = crate::bresenham::Bresenham::new( + super::bresenham::RasterCoord { + x: (from.x * scale_f64).round() as i32, + y: (from.y * scale_f64).round() as i32, + }, + super::bresenham::RasterCoord { + x: (coordinate.x * scale_f64).round() as i32, + y: (coordinate.y * scale_f64).round() as i32, + }, + ); + for coord in rasteriser { + if coord.x >= 0i32 && coord.y >= 0i32 { + let x = usize::try_from(coord.x).unwrap(); + let y = usize::try_from(coord.y).unwrap(); + raster[y][x] = true; + } + } + maybe_exterior_start = Some(coordinate); + } + + for interiors in polygon.interiors() { + let mut maybe_interior_start = None; + for coordinate in interiors { + if maybe_interior_start.is_none() { + maybe_interior_start = Some(*coordinate); + continue; + } + + let from = maybe_interior_start.unwrap(); + #[expect( + clippy::as_conversions, + clippy::cast_possible_truncation, + reason = "Gotta rasterise" + )] + let rasteriser = crate::bresenham::Bresenham::new( + super::bresenham::RasterCoord { + x: (from.x * scale_f64).round() as i32, + y: (from.y * scale_f64).round() as i32, + }, + super::bresenham::RasterCoord { + x: (coordinate.x * scale_f64).round() as i32, + y: (coordinate.y * scale_f64).round() as i32, + }, + ); + for coord in rasteriser { + if coord.x >= 0i32 && coord.y >= 0i32 { + let x = usize::try_from(coord.x).unwrap(); + let y = usize::try_from(coord.y).unwrap(); + raster[y][x] = true; + } + } + maybe_interior_start = Some(*coordinate); + } + } + } + + let mut ascii = Vec::new(); + let row_count = raster_width.div_euclid(2); + #[expect(clippy::needless_range_loop, reason = "clippy is wrong")] + for row in 0..row_count { + let mut viewshed_line = String::new(); + for x in 0..raster_width { + let y = row * 2; + let upper = raster[y][x]; + let lower = raster[y + 1][x]; + let character = match (upper, lower) { + (true, true) => ' ', + (false, true) => '▀', + (true, false) => '▄', + (false, false) => '█', + }; + viewshed_line.push(character); + } + ascii.push(viewshed_line); + } + + ascii +} + +/// # Panics +#[inline] +#[expect(clippy::print_stderr, reason = "This is for tests")] +pub fn assert_rasterised(actual: &[String], expected: &[&str]) { + if actual != expected { + eprintln!("Actual:"); + eprint!("{}", actual.join("\n")); + eprintln!(); + eprintln!(); + eprintln!("Expected:"); + eprint!("{}", expected.join("\n")); + eprintln!(); + panic!("Viewsheds do not match"); + } +} diff --git a/crates/total-viewsheds/src/output/bresenham.rs b/crates/tvs-lib/src/bresenham.rs similarity index 96% rename from crates/total-viewsheds/src/output/bresenham.rs rename to crates/tvs-lib/src/bresenham.rs index 7c9109c..5d15478 100644 --- a/crates/total-viewsheds/src/output/bresenham.rs +++ b/crates/tvs-lib/src/bresenham.rs @@ -1,4 +1,4 @@ -#![cfg(test)] +//! Rasterise lines using the Bresenham algorithm. #[derive(Clone, Copy, Debug)] /// A raster coodinate. @@ -38,7 +38,7 @@ pub(crate) struct Bresenham { impl Bresenham { /// Instantiate. - pub(crate) fn new(from: RasterCoord, to: RasterCoord) -> Self { + pub(crate) const fn new(from: RasterCoord, to: RasterCoord) -> Self { let delta = RasterCoord { x: (to.x - from.x).abs(), y: (to.y - from.y).abs(), diff --git a/crates/tvs-lib/src/lib.rs b/crates/tvs-lib/src/lib.rs index cde6056..45498b0 100644 --- a/crates/tvs-lib/src/lib.rs +++ b/crates/tvs-lib/src/lib.rs @@ -1,5 +1,7 @@ //! Code we want to share amongst projects. +pub mod ascii; +mod bresenham; pub mod dem; pub mod metadata; pub mod projector; diff --git a/crates/viewshed-reconstructor/Cargo.toml b/crates/viewshed-reconstructor/Cargo.toml new file mode 100644 index 0000000..68c8f39 --- /dev/null +++ b/crates/viewshed-reconstructor/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "viewshed-reconstructor" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib", "rlib"] + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tracing.workspace = true +tracing-subscriber.workspace = true + +[target.'cfg(target_arch = "wasm32")'.dependencies] +console_error_panic_hook = "0.1.7" +js-sys = "0.3.103" +wasm-bindgen = "0.2" + +[dev-dependencies] +geo.workspace = true +tvs-lib = { path = "../tvs-lib", version = "0.1.0" } + +[lints] +workspace = true + diff --git a/crates/viewshed-reconstructor/README.md b/crates/viewshed-reconstructor/README.md new file mode 100644 index 0000000..34a8772 --- /dev/null +++ b/crates/viewshed-reconstructor/README.md @@ -0,0 +1,18 @@ +# Viewshed Reconstruction + +This crate is for reconstucting viewsheds from their raw "polar segments". + +It is used for both in the Rust backend for outputting final viewsheds. And it is used in browser +frontends for rendering viewsheds in maps. + +How to compile for WASM: +``` +rustup target add wasm32-unknown-unknown +cargo install wasm-pack + +wasm-pack build \ + --target web \ + --out-dir /publicish/Workspace/viewview/website/src/lib/viewshed-reconstructor \ + crates/viewshed-reconstructor +``` + diff --git a/crates/total-viewsheds/src/output/viewsheds/growable_polygon.rs b/crates/viewshed-reconstructor/src/growable_polygon.rs similarity index 88% rename from crates/total-viewsheds/src/output/viewsheds/growable_polygon.rs rename to crates/viewshed-reconstructor/src/growable_polygon.rs index 39fb8f7..9fb7562 100644 --- a/crates/total-viewsheds/src/output/viewsheds/growable_polygon.rs +++ b/crates/viewshed-reconstructor/src/growable_polygon.rs @@ -1,12 +1,10 @@ //! Growable polygons are polygons constructed one angle at a time from polar segments. -use color_eyre::eyre::{ContextCompat as _, Result, bail}; - -use crate::output::viewsheds::vertices::Opening; +use crate::vertices::Opening; /// A polygon that grows, but only from its anti-clockwise facing side. #[derive(Debug, Clone)] -pub(crate) struct GrowablePolygon { +pub struct GrowablePolygon { /// The main exterior vertices. pub vertices: Vec, /// Interiore holes within the polygon. @@ -26,10 +24,7 @@ pub(crate) struct GrowablePolygon { impl GrowablePolygon { /// Create a new growable polygon. It always begins with a single polar segment converted to /// its euclidean coordinates. - pub(crate) fn new( - segment_vertices: &crate::output::viewsheds::segment_polygon::Vertices, - angle: f32, - ) -> Self { + pub(crate) fn new(segment_vertices: &super::segment_polygon::Vertices, angle: f32) -> Self { let mut vertices = Vec::new(); for (segment_index, segment_vertex) in segment_vertices.vertices.iter().enumerate() { let opening = match segment_index { @@ -80,10 +75,7 @@ impl GrowablePolygon { /// w└──┘x w└──┘x /// /// (abcde) + (wxyz) = (wxyzcdeb) - fn new_for_insertion( - segment_vertices: &crate::output::viewsheds::segment_polygon::Vertices, - angle: f32, - ) -> Self { + fn new_for_insertion(segment_vertices: &super::segment_polygon::Vertices, angle: f32) -> Self { let mut polygon = Self::new(segment_vertices, angle); #[expect( @@ -106,18 +98,19 @@ impl GrowablePolygon { /// polygon as follows: /// * `Opening::NewStart/NewEnd` become `Opening::Start/End`. /// * `Opening::Start/End` become `Opening::Null`. - pub(crate) fn downgrade_openings(&mut self) -> Result<()> { + pub(crate) fn downgrade_openings(&mut self) { + #[cfg(not(target_arch = "wasm32"))] tracing::trace!("Downgrading openings"); let mut iterator = self.vertices.iter_mut().rev(); while let Some(vertex) = iterator.next() { match vertex.opening { Opening::Null | Opening::GenesisStart(_) | Opening::GenesisEnd(_) => {} Opening::End(_) => { - bail!("Dangling `Opening`"); + panic!("Dangling `Opening`"); } Opening::Start(_) => { let Some(next) = iterator.next() else { - bail!("`Opening::Start` without adjacent end"); + panic!("`Opening::Start` without adjacent end"); }; vertex.opening = Opening::Null; @@ -131,8 +124,6 @@ impl GrowablePolygon { } } } - - Ok(()) } /// Insert new vertices into the polygon. Can be either a segment or an entire other growable @@ -145,8 +136,8 @@ impl GrowablePolygon { &mut self, base_vertices_range: std::ops::Range, joining_vertices: Vec, - ) -> Result<()> { - let old_start = self.extract_old_start(base_vertices_range.end)?; + ) { + let old_start = self.extract_old_start(base_vertices_range.end); let removed_vertices: Vec = self .vertices @@ -155,35 +146,32 @@ impl GrowablePolygon { self.vertices .get_mut(base_vertices_range.start) - .context("Couldn't get new opening start index")? + .expect("Couldn't get new opening start index") .opening = old_start; self.dedup_vertices_but_keep_openings(); self.create_holes(&removed_vertices); - - Ok(()) } /// Insert a segment into the polygon. pub(crate) fn join_segment( &mut self, - segment_vertices: &crate::output::viewsheds::segment_polygon::Vertices, + segment_vertices: &super::segment_polygon::Vertices, vertices_range: std::ops::Range, angle: f32, - ) -> Result<()> { + ) { + #[cfg(not(target_arch = "wasm32"))] tracing::trace!( "Splicing new segment ({:?}) at: {vertices_range:?}", segment_vertices.distances ); let segment = Self::new_for_insertion(segment_vertices, angle); - self.join(vertices_range, segment.vertices)?; + self.join(vertices_range, segment.vertices); if segment.furthest_opening < self.furthest_opening { self.furthest_opening = segment.furthest_opening; } - - Ok(()) } /// Insert another polygon into the `self` polygon, where `self` isn't the final polygon. @@ -207,7 +195,7 @@ impl GrowablePolygon { &mut self, base_vertices_range: std::ops::Range, joining_polygon: &mut Self, - ) -> Result<()> { + ) { let mut maybe_joining_new_end_index = None; for (index, vertex) in joining_polygon.vertices.iter_mut().enumerate().rev() { // Find where in the joining polygon we are opening up to be joined by the base polygon. @@ -224,7 +212,7 @@ impl GrowablePolygon { } } let Some(joining_new_end_index) = maybe_joining_new_end_index else { - bail!("Couldn't find `Opening::NewEnd` for joining polygon"); + panic!("Couldn't find `Opening::NewEnd` for joining polygon"); }; // We can assume that the vertex at which we join the incoming polygon is always 1 before @@ -235,12 +223,13 @@ impl GrowablePolygon { // is joined. joining_polygon.vertices.rotate_left(joining_vertices_entry); + #[cfg(not(target_arch = "wasm32"))] tracing::trace!( "Splicing existing polygon at {base_vertices_range:?}, \ joining polygon: {joining_polygon:#?}, rotated at {joining_vertices_entry}", ); - self.join(base_vertices_range, joining_polygon.vertices.clone())?; + self.join(base_vertices_range, joining_polygon.vertices.clone()); self.holes.extend(joining_polygon.holes.clone()); @@ -251,8 +240,6 @@ impl GrowablePolygon { if joining_polygon.is_created_at_angle_0 { self.is_created_at_angle_0 = true; } - - Ok(()) } /// Insert a starting polygon (from angle 0) into a final polygon (from angle ~360). @@ -261,7 +248,8 @@ impl GrowablePolygon { base_vertices_range: std::ops::Range, joining_vertices_range: std::ops::Range, joining_polygon: &mut Self, - ) -> Result<()> { + ) { + #[cfg(not(target_arch = "wasm32"))] tracing::trace!( "Splicing final polygon at {base_vertices_range:?}, \ with: {:#?} at {joining_vertices_range:?}", @@ -272,12 +260,12 @@ impl GrowablePolygon { joining_polygon .vertices .get_mut(joining_vertices_range.start) - .context("Bad joining vertex index")? + .expect("Bad joining vertex index") .opening = Opening::Null; joining_polygon .vertices .get_mut(joining_vertices_range.end) - .context("Bad joining vertex index")? + .expect("Bad joining vertex index") .opening = Opening::Null; // Rotating achieves the effect of unlooping the polygon at the point at which the polygon @@ -286,11 +274,9 @@ impl GrowablePolygon { .vertices .rotate_left(joining_vertices_range.start); - self.join(base_vertices_range, joining_polygon.vertices.clone())?; + self.join(base_vertices_range, joining_polygon.vertices.clone()); self.holes.extend(joining_polygon.holes.clone()); - - Ok(()) } /// Join a polygon into itself. @@ -305,6 +291,7 @@ impl GrowablePolygon { right_range.start..left_range.start }; + #[cfg(not(target_arch = "wasm32"))] tracing::trace!( "Splicing polygon into itself at: {range:?} ({left_range:?}/{right_range:?})" ); @@ -315,16 +302,16 @@ impl GrowablePolygon { } /// Extract the starting vertex of an opening that has just been joined to. - pub(crate) fn extract_old_start(&mut self, index: usize) -> Result { + pub(crate) fn extract_old_start(&mut self, index: usize) -> Opening { let vertex = self .vertices .get_mut(index) - .context("Bad index for old opening start")?; + .expect("Bad index for old opening start"); let old_start = vertex.opening.clone(); vertex.opening = Opening::Null; - Ok(old_start) + old_start } /// Create interior holes from the vertices that were removed for the join. @@ -333,6 +320,7 @@ impl GrowablePolygon { for hole in holes { if hole.iter().any(|vertex| !vertex.is_centre()) { + #[cfg(not(target_arch = "wasm32"))] tracing::trace!("Hole: {hole:?}"); self.holes.push(hole.to_vec()); } @@ -349,25 +337,27 @@ impl GrowablePolygon { } /// Convert the polygon to the `geo` crate's representation. Ready for exporting to `GeoJSON`. - pub(crate) fn to_geo_polygon(&self) -> geo::Polygon { - let holes: Vec = self + pub fn to_polygon(&self) -> crate::polygon::Polygon { + let holes: Vec> = self .holes .iter() .map(|hole| { - geo::LineString( - hole.iter() - .map(|vertex| vertex.coordinate) - .collect::>(), - ) + hole.iter() + .map(|vertex| vertex.coordinate) + .collect::>() }) .collect(); - let vertices: Vec = self + let vertices: Vec = self .vertices .iter() .map(|vertex| vertex.coordinate) .collect(); - geo::Polygon::new(geo::LineString(vertices), holes) + + crate::polygon::Polygon { + exterior: vertices, + interior: holes, + } } /// Remove adjacent vertices that are either identical or extremely similar. This reduces the @@ -392,7 +382,10 @@ impl GrowablePolygon { } /// Are the two coordinates identical or as good as identical. - fn are_coordinates_within_tolerance(left: &geo::Coord, right: &geo::Coord) -> bool { + fn are_coordinates_within_tolerance( + left: &crate::polygon::Coordinate, + right: &crate::polygon::Coordinate, + ) -> bool { let tolerance = 1e-6f64; (left.x - right.x).abs() < tolerance && (left.y - right.y).abs() < tolerance } diff --git a/crates/viewshed-reconstructor/src/joiner.rs b/crates/viewshed-reconstructor/src/joiner.rs new file mode 100644 index 0000000..fd02d57 --- /dev/null +++ b/crates/viewshed-reconstructor/src/joiner.rs @@ -0,0 +1,99 @@ +//! Join segments and polygons to existing growable polygons. + +mod last; +mod normal; + +/// Keeps track of active and completed polygons within a viewshed. +#[derive(Default)] +pub struct Joiner { + /// Polygons that don't intersect with the current angle. They don't need to be checked against + /// new segments. + completed: Vec, + /// Polygons that intersect with the current angle. They must be checked to see if any of their + /// openings touch any of the segments of the current angle. + active: Vec, +} + +impl Joiner { + /// Join segments into a collection of polygons. + /// + /// # Errors + /// If joining segments fails. + #[inline] + #[must_use] + pub fn join( + data: &[Vec], + dem_scale: f32, + ) -> Vec { + #[expect( + clippy::as_conversions, + clippy::cast_precision_loss, + reason = "The angle count should never strain the f32 mantissa" + )] + let angle_count = data.len() as f32; + let angle_scale = angle_count / 360.0; + + let mut joiner = Self { + completed: Vec::new(), + active: Vec::new(), + }; + + for (anglish, segments) in data.iter().enumerate() { + #[expect( + clippy::as_conversions, + clippy::cast_precision_loss, + reason = "The angle count should never strain the f32 mantissa" + )] + let angle = anglish as f32 / angle_scale; + + joiner.build_angle(angle, angle_scale, segments, dem_scale); + } + + joiner.build_final_angle(); + joiner.move_all_active_polygons_to_completed(); + for polygon in &mut joiner.completed { + polygon.dedup_vertices_ignore_openings(); + } + + joiner.completed + } +} + +#[cfg(test)] +fn rasterise_multi_polygon( + multi_polygon: Vec, +) -> Vec { + let width = 12u32; + let centre = f64::from(width.div_euclid(2)); + + let mut multi_polygons_geo = Vec::new(); + + for polygon in multi_polygon { + let mut line_exterior = Vec::new(); + for coordinate in polygon.to_polygon().exterior { + let foo = geo::Coord { + x: coordinate.x + centre, + y: coordinate.y + centre, + }; + line_exterior.push(foo); + } + + let mut holes = Vec::new(); + for hole in polygon.to_polygon().interior { + let mut line = Vec::new(); + for coordinate in hole { + let foo = geo::Coord { + x: coordinate.x + centre, + y: coordinate.y + centre, + }; + line.push(foo); + } + holes.push(geo::LineString::from(line)); + } + + let exterior = geo::LineString::from(line_exterior); + multi_polygons_geo.push(geo::Polygon::new(exterior, holes)); + } + + tvs_lib::ascii::rasterise_multi_polygon_geo(&geo::MultiPolygon::new(multi_polygons_geo), width) +} diff --git a/crates/total-viewsheds/src/output/viewsheds/joiner/last.rs b/crates/viewshed-reconstructor/src/joiner/last.rs similarity index 74% rename from crates/total-viewsheds/src/output/viewsheds/joiner/last.rs rename to crates/viewshed-reconstructor/src/joiner/last.rs index 236efea..a5f2f3f 100644 --- a/crates/total-viewsheds/src/output/viewsheds/joiner/last.rs +++ b/crates/viewshed-reconstructor/src/joiner/last.rs @@ -2,23 +2,17 @@ //! complicated, but it's okay if the code is less efficient as it only happens once per viewshed //! reconstruction. -use color_eyre::eyre::{ContextCompat as _, Result, bail}; -use itertools::Itertools as _; - -use crate::output::viewsheds::vertices::Opening; +use crate::vertices::Opening; impl super::Joiner { /// The final angle faces the challenge of joining polygons together from the first angle. - pub(crate) fn build_final_angle(&mut self) -> Result<()> { + pub(crate) fn build_final_angle(&mut self) { + #[cfg(not(target_arch = "wasm32"))] tracing::trace!("Building viewshed for final angle"); - let timing = std::time::Instant::now(); - self.active - .iter() - .sorted_by_key(|polygon| polygon.furthest_opening); + self.active.sort_by_key(|polygon| polygon.furthest_opening); self.completed - .iter() - .sorted_by_key(|polygon| polygon.furthest_opening); + .sort_by_key(|polygon| polygon.furthest_opening); let starting_polygon_indexes: Vec = self .completed @@ -28,20 +22,22 @@ impl super::Joiner { .map(|item| item.0) .collect(); - tracing::debug!(""); - let total_polygons = starting_polygon_indexes.len() + self.active.len(); - tracing::debug!("Final angle, polygons to check: {}", total_polygons); + #[cfg(not(target_arch = "wasm32"))] + { + tracing::debug!(""); + let total_polygons = starting_polygon_indexes.len() + self.active.len(); + tracing::debug!("Final angle, polygons to check: {}", total_polygons); + tracing::trace!( + "Final angle, completed polygon (started at angle 0) indices: {:?}. Active polygons: {:?}", + starting_polygon_indexes, + self.active.len() + ); + }; - tracing::trace!( - "Final angle, completed polygon (started at angle 0) indices: {:?}. Active polygons: {:?}", - starting_polygon_indexes, - self.active.len() - ); let mut touching_starting_polygons = Vec::new(); for starting_polygon_index in starting_polygon_indexes { for final_polygon_index in 0..self.active.len() { - if self - .handle_touching_polygons(final_polygon_index, Some(starting_polygon_index))? + if self.handle_touching_polygons(final_polygon_index, Some(starting_polygon_index)) { touching_starting_polygons.push(starting_polygon_index); } @@ -49,6 +45,7 @@ impl super::Joiner { } let polygons_to_remove = touching_starting_polygons.iter().rev(); + #[cfg(not(target_arch = "wasm32"))] tracing::debug!( "Removing final joined polygons: {:?}", polygons_to_remove.clone().collect::>() @@ -66,12 +63,8 @@ impl super::Joiner { .collect(); for self_joining_polygon_index in self_joining_polygon_indexes { - self.handle_touching_polygons(self_joining_polygon_index, None)?; + self.handle_touching_polygons(self_joining_polygon_index, None); } - - tracing::debug!("Final angle done in {:?}", timing.elapsed()); - - Ok(()) } /// Check whether a final and a starting polygon are touching. @@ -79,7 +72,8 @@ impl super::Joiner { &mut self, final_polygon_index: usize, maybe_starting_polygon_index: Option, - ) -> Result { + ) -> bool { + #[cfg(not(target_arch = "wasm32"))] tracing::debug!( "Checking final polygon {final_polygon_index} \ against starting polygon {maybe_starting_polygon_index:?}" @@ -88,13 +82,13 @@ impl super::Joiner { let vertices_clone = if let Some(starting_polygon_index) = maybe_starting_polygon_index { self.completed .get_mut(starting_polygon_index) - .context("Bad polygon index")? + .expect("Bad polygon index") .vertices .clone() } else { self.active .get_mut(final_polygon_index) - .context("Bad polygon index")? + .expect("Bad polygon index") .vertices .clone() }; @@ -108,18 +102,19 @@ impl super::Joiner { | Opening::Start(_) | Opening::End(_) => (), Opening::GenesisStart(_) => { - bail!("Dangling `Opening::GenesisStart`"); + panic!("Dangling `Opening::GenesisStart`"); } Opening::GenesisEnd(start) => { let Some(next) = iterator.next() else { - bail!("`Opening::GenesisEnd` without a following vertex"); + panic!("`Opening::GenesisEnd` without a following vertex"); }; let Opening::GenesisStart(end) = next.1.opening else { + #[cfg(not(target_arch = "wasm32"))] tracing::error!( "Bad opening ({:?}) in polygon: {vertices_clone:#?}", next.1.opening ); - bail!("`Opening::GenesisEnd` not followed by `Opening::GenesisStart`"); + panic!("`Opening::GenesisEnd` not followed by `Opening::GenesisStart`"); }; let next_index = next.0; @@ -132,17 +127,17 @@ impl super::Joiner { joining_distances_range, maybe_starting_polygon_index, joining_vertices_range, - )?; + ); if is_touching && maybe_starting_polygon_index.is_some() { - self.handle_touching_polygons(final_polygon_index, None)?; - return Ok(true); + self.handle_touching_polygons(final_polygon_index, None); + return true; } } } } - Ok(false) + false } /// Join a final polygon to either itself or a starting polygon. @@ -152,7 +147,8 @@ impl super::Joiner { joining_opening_range: std::ops::Range, maybe_joining_polygon_index: Option, joining_vertices_range: std::ops::Range, - ) -> Result { + ) -> bool { + #[cfg(not(target_arch = "wasm32"))] tracing::debug!( "Checking base polygon {base_polygon_index} \ opening indices: {joining_vertices_range:?}" @@ -161,7 +157,7 @@ impl super::Joiner { let base_polygon = self .active .get_mut(base_polygon_index) - .context("Bad polygon index")?; + .expect("Bad polygon index"); #[expect( clippy::indexing_slicing, @@ -173,45 +169,49 @@ impl super::Joiner { let Some(base_vertices_range) = super::super::vertices::find_contact(&base_polygon.vertices, &joining_opening_range) else { - return Ok(false); + return false; }; match maybe_joining_polygon { Some(joining_polygon) => { + #[cfg(not(target_arch = "wasm32"))] tracing::trace!("Final angle, polygon BEFORE: {:#?}", base_polygon); base_polygon.join_starting_polygon( base_vertices_range, joining_vertices_range, joining_polygon, - )?; + ); + #[cfg(not(target_arch = "wasm32"))] tracing::trace!( "Final angle, polygon AFTER joined polygon: {:#?}", base_polygon ); } None => { + #[cfg(not(target_arch = "wasm32"))] tracing::trace!("Final angle, self-polygon BEFORE: {:#?}", base_polygon); base_polygon.join_self(joining_vertices_range, base_vertices_range); + #[cfg(not(target_arch = "wasm32"))] tracing::trace!("Final angle, self-polygon AFTER: {:#?}", base_polygon); } } - Ok(true) + true } } #[cfg(test)] mod test { + use tvs_lib::ascii::assert_rasterised; + + use crate::joiner::{Joiner, rasterise_multi_polygon}; + #[test] fn final_segment_joins_starting_segment_simple() { - crate::setup_logging().unwrap(); - let part = vec![crate::storage::segments::Segment::new(2, 2)]; - let joined = crate::output::viewsheds::joiner::build_viewshed_polygon( - &[part.clone(), vec![], vec![], vec![], vec![], part], - 1.0, - ) - .unwrap(); - let actual = crate::output::ascii::rasterise(joined); + crate::setup_logging(); + let part = vec![crate::segment::Segment::new(2, 2)]; + let joined = Joiner::join(&[part.clone(), vec![], vec![], vec![], vec![], part], 1.0); + let actual = rasterise_multi_polygon(joined); let expected = [ "████████████████████████", "████████████████████████", @@ -226,28 +226,27 @@ mod test { "████████████████████████", "████████████████████████", ]; - crate::output::ascii::assert_rasterised(&actual, &expected); + assert_rasterised(&actual, &expected); } #[test] fn inherit_holes_final() { - crate::setup_logging().unwrap(); - let joined = crate::output::viewsheds::joiner::build_viewshed_polygon( + crate::setup_logging(); + let joined = Joiner::join( &[ - vec![crate::storage::segments::Segment::new(0, 3)], + vec![crate::segment::Segment::new(0, 3)], vec![ - crate::storage::segments::Segment::new(0, 1), - crate::storage::segments::Segment::new(2, 1), + crate::segment::Segment::new(0, 1), + crate::segment::Segment::new(2, 1), ], - vec![crate::storage::segments::Segment::new(0, 3)], + vec![crate::segment::Segment::new(0, 3)], vec![], vec![], - vec![crate::storage::segments::Segment::new(0, 5)], + vec![crate::segment::Segment::new(0, 5)], ], 1.0, - ) - .unwrap(); - let actual = crate::output::ascii::rasterise(joined); + ); + let actual = rasterise_multi_polygon(joined); let expected = [ "████████████████████████", "████████████ ▀▀█████████", @@ -262,23 +261,19 @@ mod test { "████████████████████████", "████████████████████████", ]; - crate::output::ascii::assert_rasterised(&actual, &expected); + assert_rasterised(&actual, &expected); } #[test] fn final_segment_joins_2_starting_segments() { - crate::setup_logging().unwrap(); - let long = vec![crate::storage::segments::Segment::new(0, 4)]; + crate::setup_logging(); + let long = vec![crate::segment::Segment::new(0, 4)]; let starting = vec![ - crate::storage::segments::Segment::new(0, 1), - crate::storage::segments::Segment::new(2, 1), + crate::segment::Segment::new(0, 1), + crate::segment::Segment::new(2, 1), ]; - let joined = crate::output::viewsheds::joiner::build_viewshed_polygon( - &[starting, vec![], vec![], vec![], vec![], long], - 1.0, - ) - .unwrap(); - let actual = crate::output::ascii::rasterise(joined); + let joined = Joiner::join(&[starting, vec![], vec![], vec![], vec![], long], 1.0); + let actual = rasterise_multi_polygon(joined); let expected = [ "████████████████████████", "████████████████████████", @@ -293,23 +288,19 @@ mod test { "████████████████████████", "████████████████████████", ]; - crate::output::ascii::assert_rasterised(&actual, &expected); + assert_rasterised(&actual, &expected); } #[test] fn final_segment_joins_starting_segment_with_2_openings() { - crate::setup_logging().unwrap(); - let long = vec![crate::storage::segments::Segment::new(0, 5)]; + crate::setup_logging(); + let long = vec![crate::segment::Segment::new(0, 5)]; let starting = vec![ - crate::storage::segments::Segment::new(2, 1), - crate::storage::segments::Segment::new(4, 1), + crate::segment::Segment::new(2, 1), + crate::segment::Segment::new(4, 1), ]; - let joined = crate::output::viewsheds::joiner::build_viewshed_polygon( - &[starting, long.clone(), vec![], vec![], vec![], long], - 1.0, - ) - .unwrap(); - let actual = crate::output::ascii::rasterise(joined); + let joined = Joiner::join(&[starting, long.clone(), vec![], vec![], vec![], long], 1.0); + let actual = rasterise_multi_polygon(joined); let expected = [ "████████████████████████", "████████████ ▀▀█████████", @@ -324,15 +315,15 @@ mod test { "████████████ ▀▀▄▄███████", "████████████▄███████████", ]; - crate::output::ascii::assert_rasterised(&actual, &expected); + assert_rasterised(&actual, &expected); } #[test] fn final_segment_joins_starting_segment_complex() { - crate::setup_logging().unwrap(); - let long = vec![crate::storage::segments::Segment::new(0, 4)]; - let short = vec![crate::storage::segments::Segment::new(2, 2)]; - let joined = crate::output::viewsheds::joiner::build_viewshed_polygon( + crate::setup_logging(); + let long = vec![crate::segment::Segment::new(0, 4)]; + let short = vec![crate::segment::Segment::new(2, 2)]; + let joined = Joiner::join( &[ short.clone(), long.clone(), @@ -344,9 +335,8 @@ mod test { short, ], 1.0, - ) - .unwrap(); - let actual = crate::output::ascii::rasterise(joined); + ); + let actual = rasterise_multi_polygon(joined); let expected = [ "████████████████████████", "████████████████████████", @@ -361,16 +351,16 @@ mod test { "████████████████████████", "████████████████████████", ]; - crate::output::ascii::assert_rasterised(&actual, &expected); + assert_rasterised(&actual, &expected); } #[test] fn a_ring() { - crate::setup_logging().unwrap(); - let part = vec![crate::storage::segments::Segment::new(2, 2)]; + crate::setup_logging(); + let part = vec![crate::segment::Segment::new(2, 2)]; let ring = vec![part; 10]; - let joined = crate::output::viewsheds::joiner::build_viewshed_polygon(&ring, 1.0).unwrap(); - let actual = crate::output::ascii::rasterise(joined); + let joined = Joiner::join(&ring, 1.0); + let actual = rasterise_multi_polygon(joined); let expected = [ "████████████████████████", "████████████████████████", @@ -385,19 +375,19 @@ mod test { "███████████▄▄▄██████████", "████████████████████████", ]; - crate::output::ascii::assert_rasterised(&actual, &expected); + assert_rasterised(&actual, &expected); } #[test] fn rings() { - crate::setup_logging().unwrap(); + crate::setup_logging(); let part = vec![ - crate::storage::segments::Segment::new(1, 1), - crate::storage::segments::Segment::new(3, 1), + crate::segment::Segment::new(1, 1), + crate::segment::Segment::new(3, 1), ]; let ring = vec![part; 10]; - let joined = crate::output::viewsheds::joiner::build_viewshed_polygon(&ring, 1.0).unwrap(); - let actual = crate::output::ascii::rasterise(joined); + let joined = Joiner::join(&ring, 1.0); + let actual = rasterise_multi_polygon(joined); let expected = [ "████████████████████████", "████████████████████████", @@ -412,6 +402,6 @@ mod test { "███████████▄▄▄██████████", "████████████████████████", ]; - crate::output::ascii::assert_rasterised(&actual, &expected); + assert_rasterised(&actual, &expected); } } diff --git a/crates/total-viewsheds/src/output/viewsheds/joiner/normal.rs b/crates/viewshed-reconstructor/src/joiner/normal.rs similarity index 75% rename from crates/total-viewsheds/src/output/viewsheds/joiner/normal.rs rename to crates/viewshed-reconstructor/src/joiner/normal.rs index 0a7e9eb..0c54bea 100644 --- a/crates/total-viewsheds/src/output/viewsheds/joiner/normal.rs +++ b/crates/viewshed-reconstructor/src/joiner/normal.rs @@ -2,56 +2,54 @@ //! are the ones that occur for every angle other than the final one. Therefore these should be //! simpler and faster. -use color_eyre::eyre::Result; - impl super::Joiner { /// Build the viewshed for a single angle. pub(crate) fn build_angle( &mut self, angle: f32, angle_scale: f32, - segments: &[crate::storage::segments::Segment], + segments: &[crate::segment::Segment], dem_scale: f32, - ) -> Result<()> { - tracing::debug!(""); - tracing::debug!("Building viewshed for angle: {angle}"); - tracing::debug!( - "Polygon counts, completed: {}, active: {}. Segments: {}", - self.completed.len(), - self.active.len(), - segments.len() - ); + ) { + #[cfg(not(target_arch = "wasm32"))] + { + tracing::debug!(""); + tracing::debug!("Building viewshed for angle: {angle}"); + tracing::debug!( + "Polygon counts, completed: {}, active: {}. Segments: {}", + self.completed.len(), + self.active.len(), + segments.len() + ); + }; self.prepare_active_polygons(); let mut new_polygons = Vec::new(); - let segment = crate::output::viewsheds::segment_polygon::SegmentPolygon { + let segment = crate::segment_polygon::SegmentPolygon { dem_scale, angle, angle_scale, }; - let timing = std::time::Instant::now(); for (segment_index, polar_segment) in segments.iter().enumerate() { let start = u32::from(polar_segment.start()); let end = u32::from(polar_segment.start() + polar_segment.distance()); + #[cfg(not(target_arch = "wasm32"))] tracing::debug!("Segment {segment_index}, distances range: {:?}", start..end); - let polygon_segment = - crate::output::viewsheds::segment_polygon::Vertices::new(&segment, start, end); + let polygon_segment = crate::segment_polygon::Vertices::new(&segment, start, end); let mut is_segment_touching_anything = false; let mut maybe_joining_polygon_index = None; let mut joined_polygons_to_remove = Vec::new(); for active_polygon_index in 0..self.active.len() { - let touchging_timing = std::time::Instant::now(); - let is_segment_touching_this_polygon = self.join_segment( active_polygon_index, &polygon_segment, maybe_joining_polygon_index, angle, - )?; + ); #[expect( clippy::indexing_slicing, @@ -70,13 +68,9 @@ impl super::Joiner { } else { maybe_joining_polygon_index = None; } - - tracing::debug!( - "Segment checked against active {active_polygon_index} in {:?}", - touchging_timing.elapsed() - ); } + #[cfg(not(target_arch = "wasm32"))] tracing::debug!("Removing joined polygons: {joined_polygons_to_remove:?}"); for joined_polygon_index in joined_polygons_to_remove.iter().rev() { self.active.remove(*joined_polygon_index); @@ -86,41 +80,35 @@ impl super::Joiner { new_polygons.push(Self::create_new_polygon_from_untouched_segment( &polygon_segment, angle, - )?); + )); } } for polygon in &mut self.active { - polygon.downgrade_openings()?; + polygon.downgrade_openings(); } - tracing::debug!("Angle {angle} done in {:?}", timing.elapsed()); - self.move_untouched_active_polygons_to_completed(); self.active.extend(new_polygons); - - Ok(()) } /// When a segment doesn't touch anything it becomes its own independent polygon. fn create_new_polygon_from_untouched_segment( - polygon_segment: &crate::output::viewsheds::segment_polygon::Vertices, + polygon_segment: &crate::segment_polygon::Vertices, angle: f32, - ) -> Result { + ) -> crate::growable_polygon::GrowablePolygon { + #[cfg(not(target_arch = "wasm32"))] tracing::debug!( "Segment not touching anything at angle {angle}, \ so making it its own polygon: {polygon_segment:?}" ); - let mut polygon = crate::output::viewsheds::growable_polygon::GrowablePolygon::new( - polygon_segment, - angle, - ); + let mut polygon = crate::growable_polygon::GrowablePolygon::new(polygon_segment, angle); if angle == 0.0 { polygon.is_created_at_angle_0 = true; } - polygon.downgrade_openings()?; + polygon.downgrade_openings(); - Ok(polygon) + polygon } /// Set all active polygons to untouched and order them based on their openings' distance from @@ -188,13 +176,14 @@ impl super::Joiner { fn join_segment( &mut self, growable_polygon_index: usize, - segment: &crate::output::viewsheds::segment_polygon::Vertices, + segment: &crate::segment_polygon::Vertices, maybe_joining_polygon_index: Option, angle: f32, - ) -> Result { + ) -> bool { let (base_polygon, maybe_joining_polygon) = self.get_involved_polygons(growable_polygon_index, maybe_joining_polygon_index); + #[cfg(not(target_arch = "wasm32"))] tracing::trace!( "Polygon {growable_polygon_index} BEFORE: {:#?}", base_polygon @@ -203,19 +192,21 @@ impl super::Joiner { let Some(base_vertices_range) = super::super::vertices::find_contact(&base_polygon.vertices, &segment.distances) else { - return Ok(false); + return false; }; match maybe_joining_polygon { Some(joining_polygon) => { - base_polygon.join_non_starting_polygon(base_vertices_range, joining_polygon)?; + base_polygon.join_non_starting_polygon(base_vertices_range, joining_polygon); + #[cfg(not(target_arch = "wasm32"))] tracing::trace!( "Polygon {growable_polygon_index} AFTER joined polygon: {:#?}", base_polygon ); } None => { - base_polygon.join_segment(segment, base_vertices_range, angle)?; + base_polygon.join_segment(segment, base_vertices_range, angle); + #[cfg(not(target_arch = "wasm32"))] tracing::trace!( "Polygon {growable_polygon_index} AFTER joined segment: {:#?}", base_polygon @@ -223,7 +214,7 @@ impl super::Joiner { } } - Ok(true) + true } /// Get the polygons involved in a touch check. @@ -234,8 +225,8 @@ impl super::Joiner { growable_polygon_index: usize, maybe_joining_polygon_index: Option, ) -> ( - &mut crate::output::viewsheds::growable_polygon::GrowablePolygon, - Option<&mut crate::output::viewsheds::growable_polygon::GrowablePolygon>, + &mut crate::growable_polygon::GrowablePolygon, + Option<&mut crate::growable_polygon::GrowablePolygon>, ) { if let Some(joining_polygon_index) = maybe_joining_polygon_index { #[expect( @@ -261,16 +252,16 @@ impl super::Joiner { #[cfg(test)] mod test { + use tvs_lib::ascii::assert_rasterised; + + use crate::joiner::{Joiner, rasterise_multi_polygon}; + #[test] fn multiple_angles() { - crate::setup_logging().unwrap(); - let segment = vec![crate::storage::segments::Segment::new(0, 5)]; - let joined = crate::output::viewsheds::joiner::build_viewshed_polygon( - &[segment.clone(), segment.clone(), segment], - 1.0, - ) - .unwrap(); - let actual = crate::output::ascii::rasterise(joined); + crate::setup_logging(); + let segment = vec![crate::segment::Segment::new(0, 5)]; + let joined = Joiner::join(&[segment.clone(), segment.clone(), segment], 1.0); + let actual = rasterise_multi_polygon(joined); let expected = [ "████████████████████████", "█████████████████▀██████", @@ -285,18 +276,18 @@ mod test { "███████████████▄▄ ██████", "████████████████████████", ]; - crate::output::ascii::assert_rasterised(&actual, &expected); + assert_rasterised(&actual, &expected); } #[test] fn multiple_segments() { - crate::setup_logging().unwrap(); - let main = vec![crate::storage::segments::Segment::new(0, 5)]; + crate::setup_logging(); + let main = vec![crate::segment::Segment::new(0, 5)]; let multiple = vec![ - crate::storage::segments::Segment::new(0, 2), - crate::storage::segments::Segment::new(3, 1), + crate::segment::Segment::new(0, 2), + crate::segment::Segment::new(3, 1), ]; - let joined = crate::output::viewsheds::joiner::build_viewshed_polygon( + let joined = Joiner::join( &[ main.clone(), main.clone(), @@ -306,9 +297,8 @@ mod test { main, ], 1.0, - ) - .unwrap(); - let actual = crate::output::ascii::rasterise(joined); + ); + let actual = rasterise_multi_polygon(joined); let expected = [ "████████████████████████", "████████████ ▀▀█████████", @@ -323,14 +313,14 @@ mod test { "████████▄▄▀▀█▀▀▄▄███████", "████████████▄███████████", ]; - crate::output::ascii::assert_rasterised(&actual, &expected); + assert_rasterised(&actual, &expected); } #[test] fn multiple_polygons() { - crate::setup_logging().unwrap(); - let segment = vec![crate::storage::segments::Segment::new(0, 5)]; - let joined = crate::output::viewsheds::joiner::build_viewshed_polygon( + crate::setup_logging(); + let segment = vec![crate::segment::Segment::new(0, 5)]; + let joined = Joiner::join( &[ segment.clone(), vec![], @@ -341,9 +331,8 @@ mod test { vec![], ], 1.0, - ) - .unwrap(); - let actual = crate::output::ascii::rasterise(joined); + ); + let actual = rasterise_multi_polygon(joined); let expected = [ "████████████████████████", "█████████▀▀▀▀▄ █████████", @@ -358,14 +347,14 @@ mod test { "████████████████████████", "████████████████████████", ]; - crate::output::ascii::assert_rasterised(&actual, &expected); + assert_rasterised(&actual, &expected); } #[test] fn multiple_polygons_not_touching() { - crate::setup_logging().unwrap(); - let segment = vec![crate::storage::segments::Segment::new(2, 2)]; - let joined = crate::output::viewsheds::joiner::build_viewshed_polygon( + crate::setup_logging(); + let segment = vec![crate::segment::Segment::new(2, 2)]; + let joined = Joiner::join( &[ segment.clone(), vec![], @@ -376,9 +365,8 @@ mod test { vec![], ], 1.0, - ) - .unwrap(); - let actual = crate::output::ascii::rasterise(joined); + ); + let actual = rasterise_multi_polygon(joined); let expected = [ "████████████████████████", "████████████████████████", @@ -393,20 +381,19 @@ mod test { "████████████████████████", "████████████████████████", ]; - crate::output::ascii::assert_rasterised(&actual, &expected); + assert_rasterised(&actual, &expected); } #[test] fn varying_sized_segments() { - crate::setup_logging().unwrap(); - let main = vec![crate::storage::segments::Segment::new(0, 4)]; - let variance = vec![crate::storage::segments::Segment::new(0, 2)]; - let joined = crate::output::viewsheds::joiner::build_viewshed_polygon( + crate::setup_logging(); + let main = vec![crate::segment::Segment::new(0, 4)]; + let variance = vec![crate::segment::Segment::new(0, 2)]; + let joined = Joiner::join( &[main.clone(), main.clone(), main.clone(), main, variance], 1.0, - ) - .unwrap(); - let actual = crate::output::ascii::rasterise(joined); + ); + let actual = rasterise_multi_polygon(joined); let expected = [ "████████████████████████", "████████████████████████", @@ -421,24 +408,20 @@ mod test { "██████████▄▄████████████", "████████████████████████", ]; - crate::output::ascii::assert_rasterised(&actual, &expected); + assert_rasterised(&actual, &expected); } #[test] fn untouched_openings_get_closed() { - crate::setup_logging().unwrap(); - let main = vec![crate::storage::segments::Segment::new(0, 4)]; + crate::setup_logging(); + let main = vec![crate::segment::Segment::new(0, 4)]; let pair = vec![ - crate::storage::segments::Segment::new(0, 2), - crate::storage::segments::Segment::new(3, 1), + crate::segment::Segment::new(0, 2), + crate::segment::Segment::new(3, 1), ]; - let bottom = vec![crate::storage::segments::Segment::new(0, 2)]; - let joined = crate::output::viewsheds::joiner::build_viewshed_polygon( - &[main.clone(), pair, bottom, main.clone(), main], - 1.0, - ) - .unwrap(); - let actual = crate::output::ascii::rasterise(joined); + let bottom = vec![crate::segment::Segment::new(0, 2)]; + let joined = Joiner::join(&[main.clone(), pair, bottom, main.clone(), main], 1.0); + let actual = rasterise_multi_polygon(joined); let expected = [ "████████████████████████", "████████████████████████", @@ -453,15 +436,15 @@ mod test { "██████████▄▄████████████", "████████████████████████", ]; - crate::output::ascii::assert_rasterised(&actual, &expected); + assert_rasterised(&actual, &expected); } #[test] fn centre_is_touched_after_more_than_one_angle() { - crate::setup_logging().unwrap(); - let main = vec![crate::storage::segments::Segment::new(0, 4)]; - let top = vec![crate::storage::segments::Segment::new(2, 2)]; - let joined = crate::output::viewsheds::joiner::build_viewshed_polygon( + crate::setup_logging(); + let main = vec![crate::segment::Segment::new(0, 4)]; + let top = vec![crate::segment::Segment::new(2, 2)]; + let joined = Joiner::join( &[ main.clone(), top.clone(), @@ -471,9 +454,8 @@ mod test { main, ], 1.0, - ) - .unwrap(); - let actual = crate::output::ascii::rasterise(joined); + ); + let actual = rasterise_multi_polygon(joined); let expected = [ "████████████████████████", "████████████████████████", @@ -488,20 +470,16 @@ mod test { "████████████▄███████████", "████████████████████████", ]; - crate::output::ascii::assert_rasterised(&actual, &expected); + assert_rasterised(&actual, &expected); } #[test] fn a_hole() { - crate::setup_logging().unwrap(); - let main = vec![crate::storage::segments::Segment::new(0, 4)]; - let lid = vec![crate::storage::segments::Segment::new(2, 1)]; - let joined = crate::output::viewsheds::joiner::build_viewshed_polygon( - &[main.clone(), main.clone(), lid, main.clone(), main], - 1.0, - ) - .unwrap(); - let actual = crate::output::ascii::rasterise(joined); + crate::setup_logging(); + let main = vec![crate::segment::Segment::new(0, 4)]; + let lid = vec![crate::segment::Segment::new(2, 1)]; + let joined = Joiner::join(&[main.clone(), main.clone(), lid, main.clone(), main], 1.0); + let actual = rasterise_multi_polygon(joined); let expected = [ "████████████████████████", "████████████████████████", @@ -516,24 +494,23 @@ mod test { "██████████▄▄████████████", "████████████████████████", ]; - crate::output::ascii::assert_rasterised(&actual, &expected); + assert_rasterised(&actual, &expected); } #[test] fn multiple_holes() { - crate::setup_logging().unwrap(); - let main = vec![crate::storage::segments::Segment::new(0, 6)]; + crate::setup_logging(); + let main = vec![crate::segment::Segment::new(0, 6)]; let struts = vec![ - crate::storage::segments::Segment::new(0, 1), - crate::storage::segments::Segment::new(2, 1), - crate::storage::segments::Segment::new(4, 1), + crate::segment::Segment::new(0, 1), + crate::segment::Segment::new(2, 1), + crate::segment::Segment::new(4, 1), ]; - let joined = crate::output::viewsheds::joiner::build_viewshed_polygon( + let joined = Joiner::join( &[main.clone(), main.clone(), main.clone(), struts, main], 1.0, - ) - .unwrap(); - let actual = crate::output::ascii::rasterise(joined); + ); + let actual = rasterise_multi_polygon(joined); let expected = [ "████████▀▀██████████████", "████████▀▄▄▄▄▄▀▀▀███████", @@ -548,18 +525,18 @@ mod test { "██████▄▀██████▀▀▀▄▄▄▄███", "███████▄▀▀▄▄▄▄██████████", ]; - crate::output::ascii::assert_rasterised(&actual, &expected); + assert_rasterised(&actual, &expected); } #[test] fn segment_joins_to_multiple_polygons() { - crate::setup_logging().unwrap(); + crate::setup_logging(); let polygons = vec![ - crate::storage::segments::Segment::new(2, 1), - crate::storage::segments::Segment::new(4, 1), + crate::segment::Segment::new(2, 1), + crate::segment::Segment::new(4, 1), ]; - let long = vec![crate::storage::segments::Segment::new(0, 5)]; - let joined = crate::output::viewsheds::joiner::build_viewshed_polygon( + let long = vec![crate::segment::Segment::new(0, 5)]; + let joined = Joiner::join( &[ polygons.clone(), polygons.clone(), @@ -569,9 +546,8 @@ mod test { vec![], ], 1.0, - ) - .unwrap(); - let actual = crate::output::ascii::rasterise(joined); + ); + let actual = rasterise_multi_polygon(joined); let expected = [ "████████████████████████", "██████████▀▀ ███████████", @@ -586,28 +562,24 @@ mod test { "████████▄▄▀▀▄▀▀▄▄███████", "████████████▄███████████", ]; - crate::output::ascii::assert_rasterised(&actual, &expected); + assert_rasterised(&actual, &expected); } #[test] fn join_in_distance_order() { - crate::setup_logging().unwrap(); + crate::setup_logging(); // Even though this polygon is made first and so naturally appears first in the `self.active` // list of polygons, it is in fact _nearer_ the centre and so must be joined before newer but // higher polygons. - let higher_but_made_first = crate::storage::segments::Segment::new(2, 2); + let higher_but_made_first = crate::segment::Segment::new(2, 2); - let lower = crate::storage::segments::Segment::new(0, 1); + let lower = crate::segment::Segment::new(0, 1); let first = vec![higher_but_made_first.clone()]; let second = vec![lower, higher_but_made_first]; - let long = vec![crate::storage::segments::Segment::new(0, 5)]; - let joined = crate::output::viewsheds::joiner::build_viewshed_polygon( - &[vec![], first, second, long, vec![]], - 1.0, - ) - .unwrap(); - let actual = crate::output::ascii::rasterise(joined); + let long = vec![crate::segment::Segment::new(0, 5)]; + let joined = Joiner::join(&[vec![], first, second, long, vec![]], 1.0); + let actual = rasterise_multi_polygon(joined); let expected = [ "████████████████████████", "████████▀ ██████████████", @@ -622,33 +594,32 @@ mod test { "██████████▄▄████████████", "████████████████████████", ]; - crate::output::ascii::assert_rasterised(&actual, &expected); + assert_rasterised(&actual, &expected); } #[test] fn inherit_holes_common() { - crate::setup_logging().unwrap(); + crate::setup_logging(); - let joined = crate::output::viewsheds::joiner::build_viewshed_polygon( + let joined = Joiner::join( &[ vec![], - vec![crate::storage::segments::Segment::new(0, 3)], + vec![crate::segment::Segment::new(0, 3)], vec![ - crate::storage::segments::Segment::new(0, 1), - crate::storage::segments::Segment::new(2, 1), + crate::segment::Segment::new(0, 1), + crate::segment::Segment::new(2, 1), ], vec![ - crate::storage::segments::Segment::new(0, 3), - crate::storage::segments::Segment::new(4, 1), + crate::segment::Segment::new(0, 3), + crate::segment::Segment::new(4, 1), ], - vec![crate::storage::segments::Segment::new(0, 5)], + vec![crate::segment::Segment::new(0, 5)], vec![], vec![], ], 1.0, - ) - .unwrap(); - let actual = crate::output::ascii::rasterise(joined); + ); + let actual = rasterise_multi_polygon(joined); let expected = [ "████████████████████████", "████████████████████████", @@ -663,6 +634,6 @@ mod test { "██████▄█████████████████", "████████████████████████", ]; - crate::output::ascii::assert_rasterised(&actual, &expected); + assert_rasterised(&actual, &expected); } } diff --git a/crates/viewshed-reconstructor/src/lib.rs b/crates/viewshed-reconstructor/src/lib.rs new file mode 100644 index 0000000..66b6473 --- /dev/null +++ b/crates/viewshed-reconstructor/src/lib.rs @@ -0,0 +1,35 @@ +//! Reconstruct viewsheds from polar segments. +//! +//! You could just use + +#![expect( + clippy::expect_used, + clippy::panic, + reason = "We're not using errors to keep the WASM bundle size small" +)] + +#[cfg(test)] +use tracing_subscriber::{Layer as _, layer::SubscriberExt as _, util::SubscriberInitExt as _}; + +mod growable_polygon; +pub mod joiner; +pub mod polygon; +pub mod segment; +mod segment_polygon; +mod vertices; +mod wasm; + +/// Setup logging. +#[cfg(test)] +pub(crate) fn setup_logging() { + let filters = tracing_subscriber::EnvFilter::builder() + .with_default_directive( + "total_viewsheds=debug" + .parse() + .expect("Couldn't parse log ENV filter"), + ) + .from_env_lossy(); + let filter_layer = tracing_subscriber::fmt::layer().with_filter(filters); + let tracing_setup = tracing_subscriber::registry().with(filter_layer); + tracing_setup.init(); +} diff --git a/crates/viewshed-reconstructor/src/polygon.rs b/crates/viewshed-reconstructor/src/polygon.rs new file mode 100644 index 0000000..3c0a30b --- /dev/null +++ b/crates/viewshed-reconstructor/src/polygon.rs @@ -0,0 +1,39 @@ +//! A polygon represented by simple types. Avoids having to compile the `geo` crate. + +/// A viewshed-based coordinate. +#[expect(clippy::exhaustive_structs, reason = "It will never change")] +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct Coordinate { + /// The x coordinate. + pub x: f64, + /// The y coordinate. + pub y: f64, +} + +impl Coordinate { + /// Just a shortcut for a coordinate at 0,0. + #[inline] + #[must_use] + pub const fn zero() -> Self { + Self { x: 0.0, y: 0.0 } + } + + /// Scale the x and y compenents by a factor. + #[inline] + #[must_use] + pub fn scale(&self, factor: f64) -> Self { + Self { + x: self.x * factor, + y: self.y * factor, + } + } +} + +/// A polygon with a single exterior and multiple interior "holes". +#[expect(clippy::exhaustive_structs, reason = "It will never change")] +pub struct Polygon { + /// Coordinates representing the polygon's exterior boundary. + pub exterior: Vec, + /// Coordinates representing the polygon's interior boundaries. + pub interior: Vec>, +} diff --git a/crates/viewshed-reconstructor/src/segment.rs b/crates/viewshed-reconstructor/src/segment.rs new file mode 100644 index 0000000..0cc0e42 --- /dev/null +++ b/crates/viewshed-reconstructor/src/segment.rs @@ -0,0 +1,43 @@ +//! A "segment" is a single area of visibility for a given angle on a viewshed. There may be many +//! segments per angle. + +/// `Segment` is the rho portion of a line segment in polar coordinates +/// as (`rho`: u16, `delta_rho`: u16) which are packed into a single u32 for storage. +#[expect(clippy::exhaustive_structs, reason = "This will never change.")] +#[derive(Clone, Default)] +pub struct Segment(pub u32); + +impl Segment { + /// `new` creates a `Segment` the segment's start point and the distance + #[inline] + #[must_use] + pub fn new(start: u16, distance: u16) -> Self { + // pack start/distsance into a u32 in the format of (start|distance) + let wide_start: u32 = start.into(); + let wide_distance: u32 = distance.into(); + Self((wide_start << 16) | wide_distance) + } + + /// `start` returns the starting point of the `Segment` + #[expect( + clippy::as_conversions, + reason = "the top 16 bits are guaranteed to be 0" + )] + #[inline] + #[must_use] + pub const fn start(&self) -> u16 { + (self.0 >> 16) as u16 + } + + /// `distance` returns the distance the `Segment` takes + #[expect( + clippy::as_conversions, + clippy::cast_possible_truncation, + reason = "the top 16 bits are guaranteed to be 0" + )] + #[inline] + #[must_use] + pub const fn distance(&self) -> u16 { + self.0 as u16 + } +} diff --git a/crates/total-viewsheds/src/output/viewsheds/segment_polygon.rs b/crates/viewshed-reconstructor/src/segment_polygon.rs similarity index 62% rename from crates/total-viewsheds/src/output/viewsheds/segment_polygon.rs rename to crates/viewshed-reconstructor/src/segment_polygon.rs index 5b64082..75399ee 100644 --- a/crates/total-viewsheds/src/output/viewsheds/segment_polygon.rs +++ b/crates/viewshed-reconstructor/src/segment_polygon.rs @@ -5,7 +5,7 @@ pub(crate) struct Vertices { /// The 5 vertices of a segment. The 4 corners plus a copy of the first vertex to close the /// polygon. - pub vertices: [geo::Coord; 5], + pub vertices: [crate::polygon::Coordinate; 5], /// The distances of the top and bottom of the segment from the centre. pub distances: std::ops::Range, } @@ -25,11 +25,11 @@ impl Vertices { let scale = f64::from(segment.dem_scale); let vertices = [ - bottom_left * scale, - bottom_right * scale, - top_right * scale, - top_left * scale, - bottom_left * scale, + bottom_left.scale(scale), + bottom_right.scale(scale), + top_right.scale(scale), + top_left.scale(scale), + bottom_left.scale(scale), ]; Self { @@ -51,14 +51,14 @@ pub(crate) struct SegmentPolygon { impl SegmentPolygon { /// Convert an index along a line of sight into a coordinate. - fn index_to_coordinate(&self, index: u32) -> super::viewshed::Coordinate { + fn index_to_coordinate(&self, index: u32) -> crate::polygon::Coordinate { let radians = self.angle.to_radians(); let distance = f64::from(index); - super::viewshed::Coordinate(geo::coord! { + crate::polygon::Coordinate { x: distance * f64::from(radians.cos()), - y: distance * f64::from(radians.sin()) - }) + y: distance * f64::from(radians.sin()), + } } /// Rotate a point about the centre of the viewshed. @@ -66,26 +66,25 @@ impl SegmentPolygon { clippy::suboptimal_flops, reason = "I think readability is more important?" )] - fn rotate_by(point: super::viewshed::Coordinate, angle: f64) -> geo::Coord { - let dx = point.0.x; - let dy = point.0.y; + fn rotate_by(point: crate::polygon::Coordinate, angle: f64) -> crate::polygon::Coordinate { + let dx = point.x; + let dy = point.y; let cos = angle.to_radians().cos(); let sin = angle.to_radians().sin(); - geo::coord! { + crate::polygon::Coordinate { x: dx * cos - dy * sin, - y: dx * sin + dy * cos + y: dx * sin + dy * cos, } } } #[cfg(test)] mod test { - fn builder( - viewshed: &crate::output::viewsheds::viewshed::Viewshed, - angle: f32, - ) -> crate::output::viewsheds::segment_polygon::SegmentPolygon { - crate::output::viewsheds::segment_polygon::SegmentPolygon { - dem_scale: viewshed.dem.scale, + use super::*; + + fn builder(angle: f32) -> SegmentPolygon { + SegmentPolygon { + dem_scale: 1.0, angle, angle_scale: 1.0, } @@ -93,44 +92,40 @@ mod test { #[derive(Debug)] struct VisiblePolygonFor { - pov: geo::Coord, angle: f32, opening_index: u32, closing_index: u32, } - fn make_visible_polygon_for(setup: &VisiblePolygonFor) -> Vec { - let dem = crate::run::test::make_dem(&crate::tests::fixtures::single_peak_dem()); - let viewshed = crate::output::viewsheds::viewshed::Viewshed { - dem: &dem, - pov_coord: tvs_lib::dem::Coordinate(setup.pov), - }; - let segment = builder(&viewshed, setup.angle); + fn make_visible_polygon_for(setup: &VisiblePolygonFor) -> Vec { + let segment = builder(setup.angle); let vertices = super::Vertices::new(&segment, setup.opening_index, setup.closing_index).vertices; - let polygon = geo::Polygon::new(geo::LineString(vertices.into()), vec![]); let mut polygon_as_dem_coords = Vec::new(); - for coord in &polygon.exterior().0 { - let converted_coord = viewshed - .convert_viewshed_coord_to_dem_coord( - crate::output::viewsheds::viewshed::Coordinate(*coord), - ) - .unwrap(); - polygon_as_dem_coords.push(round_coordinate(converted_coord)); + for vertex in &vertices { + let dem_coord = crate::polygon::Coordinate { + x: vertex.x + 4.0, + y: -vertex.y + 4.0, + }; + polygon_as_dem_coords.push(round_coordinate(dem_coord)); } polygon_as_dem_coords } + pub(crate) fn coord(x: f64, y: f64) -> crate::polygon::Coordinate { + crate::polygon::Coordinate { x, y } + } + fn round(float: f64) -> f64 { let factor = 10f64.powi(7); (float * factor).round() / factor } - fn round_coordinate(coordinate: geo::Coord) -> geo::Coord { - geo::coord! { - x: round(coordinate.x), - y: round(coordinate.y), + fn round_coordinate(coordinate: crate::polygon::Coordinate) -> crate::polygon::Coordinate { + crate::polygon::Coordinate { + x: round(coordinate.x), + y: round(coordinate.y), } } @@ -150,29 +145,27 @@ mod test { mod from_centre_to_top_right { use super::*; - const POV: geo::Coord = geo::coord! {x: 4.0, y: 4.0}; const ANGLE: f32 = 45.0; // The polygon we're making is `abcd` from the above guide. + #[expect(clippy::unreadable_literal, reason = "It's just a test")] #[test] fn making_a_visible_polygon() { assert_eq!( make_visible_polygon_for(&VisiblePolygonFor { - pov: POV, angle: ANGLE, opening_index: 1, closing_index: 2, }), vec![ - (4.7009079, 3.2867515), - (4.7132491, 3.2990927), - (5.4264998, 2.5981837), - (5.4018174, 2.5735014), - (4.7009079, 3.2867515) + coord(4.7009093, 3.2867496), + coord(4.7132504, 3.2990907), + coord(5.4265009, 2.5981815), + coord(5.4018185, 2.5734991), + coord(4.7009093, 3.2867496), ] .into_iter() - .map(Into::into) - .collect::>() + .collect::>() ); } } @@ -184,38 +177,36 @@ mod test { // 1 . . . . . . . . . // 2 . . . . . . . . . // 3 . . . . . . . . . + // 5 . . . . o .a . . . + // 6 . . . . . ( .d . . + // 7 . . . . . b. ) . . + // 8 . . . . . . c. . . // 4 . . . . . . . . . - // 5 . . . o .a . . . . - // 6 . . . . ( .d . . . - // 7 . . . . b. ) . . . - // 8 . . . . . c. . . . // mod from_bottom_left_to_bottom_right { use super::*; - const POV: geo::Coord = geo::coord! {x: 3.0, y: 5.0}; const ANGLE: f32 = 135.0 + 180.0; // The polygon we're making is `abcd` from the above guide. + #[expect(clippy::unreadable_literal, reason = "It's just a test")] #[test] fn making_a_visible_polygon() { assert_eq!( make_visible_polygon_for(&VisiblePolygonFor { - pov: POV, angle: ANGLE, opening_index: 1, closing_index: 2, }), vec![ - (3.7132486, 5.7009105), - (3.7009074, 5.7132517), - (4.4018163, 6.4265025), - (4.4264987, 6.4018201), - (3.7132486, 5.7009105) + coord(4.7132503, 4.7009094), + coord(4.7009091, 4.7132506), + coord(5.4018183, 5.4265011), + coord(5.4265006, 5.4018187), + coord(4.7132503, 4.7009094), ] .into_iter() - .map(Into::into) - .collect::>() + .collect::>() ); } } diff --git a/crates/total-viewsheds/src/output/viewsheds/vertices.rs b/crates/viewshed-reconstructor/src/vertices.rs similarity index 94% rename from crates/total-viewsheds/src/output/viewsheds/vertices.rs rename to crates/viewshed-reconstructor/src/vertices.rs index d212c98..0dd2a4b 100644 --- a/crates/total-viewsheds/src/output/viewsheds/vertices.rs +++ b/crates/viewshed-reconstructor/src/vertices.rs @@ -3,9 +3,9 @@ /// A vertex is a single point in a polygon. #[derive(Debug, Clone)] -pub(crate) struct Vertex { +pub struct Vertex { /// The coordinate of the vertex in euclidian space. - pub coordinate: geo::Coord, + pub coordinate: crate::polygon::Coordinate, /// The kind of opening that this vertex represents. pub opening: Opening, } @@ -22,7 +22,7 @@ impl Vertex { /// The `u32` values in the variants are for storing the polar distance from the centre. This saves /// having to do trigonometry to figure out if two openings are touching. #[derive(Eq, PartialEq, Debug, Clone)] -pub(crate) enum Opening { +pub enum Opening { /// No opening. We could also just use `Option::None`, but unwrapping it isn't so ergonomic. Null, /// This i for polygons that start at 0 degrees. It's possible that another polygon (or even @@ -117,6 +117,7 @@ pub(crate) fn find_contact( ) -> Option> { OpeningsIterator::new(vertices) .filter(|(base_opening_index, base_opening_range)| { + #[cfg(not(target_arch = "wasm32"))] tracing::debug!( "Checking if openings touch: \ index: {base_opening_index:?}, \ @@ -124,15 +125,16 @@ pub(crate) fn find_contact( joining distances: {joining_opening_range:?}", ); - let is_touching = - crate::output::viewsheds::growable_polygon::GrowablePolygon::is_touching( - base_opening_range, - joining_opening_range, - ); + let is_touching = crate::growable_polygon::GrowablePolygon::is_touching( + base_opening_range, + joining_opening_range, + ); if is_touching { + #[cfg(not(target_arch = "wasm32"))] tracing::debug!("🟢 Openings touch"); } else { + #[cfg(not(target_arch = "wasm32"))] tracing::debug!("🟡 Openings don't touch"); } @@ -153,7 +155,7 @@ mod test { fn vertex(opening: super::Opening) -> Vertex { super::Vertex { - coordinate: geo::Coord::zero(), + coordinate: crate::polygon::Coordinate::zero(), opening, } } diff --git a/crates/viewshed-reconstructor/src/wasm.rs b/crates/viewshed-reconstructor/src/wasm.rs new file mode 100644 index 0000000..72182dc --- /dev/null +++ b/crates/viewshed-reconstructor/src/wasm.rs @@ -0,0 +1,130 @@ +//! Support using this crate in the browser with WASM. + +// #![cfg(target_arch = "wasm32")] + +#[cfg(target_arch = "wasm32")] +use js_sys::Array; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + +/// WASM only supports simple types, so this is a simple representation of a polygon. +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen] +pub struct PlainPolygon { + /// The exterior coordinates of the polygon. x/y components are flattened, so the first + /// coordinate is found at [0,1]. + exterior: Vec, + /// The interior coordinates of the polygon. To find the starting/ending indices of each + /// hole see `hole_indices`. x/y components are flattened, so the first coordinate is + /// found at [0,1]. + interiors: Vec, + /// Indices of where each hole starts and ends in `interiors`. + hole_indices: Vec, +} + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen] +impl PlainPolygon { + /// Getter for `exterior`. + #[wasm_bindgen(getter)] + #[must_use] + pub fn exterior(&self) -> Vec { + self.exterior.clone() + } + + /// Getter for `interiors`. + #[wasm_bindgen(getter)] + #[must_use] + pub fn interiors(&self) -> Vec { + self.interiors.clone() + } + + /// Getter for `hole_indices`. + #[wasm_bindgen(getter)] + #[must_use] + pub fn hole_indices(&self) -> Vec { + self.hole_indices.clone() + } +} + +#[cfg(target_arch = "wasm32")] +impl PlainPolygon { + /// Instantiate. + #[inline] + #[must_use] + pub fn new(growable_polygon: &crate::growable_polygon::GrowablePolygon) -> Self { + let mut exterior = Vec::with_capacity(growable_polygon.vertices.len() * 2); + for vertex in &growable_polygon.vertices { + exterior.push(vertex.coordinate.x); + exterior.push(vertex.coordinate.y); + } + + let mut interiors = Vec::new(); + let mut hole_indices = Vec::with_capacity(growable_polygon.holes.len()); + + for hole in &growable_polygon.holes { + hole_indices.push( + u32::try_from(interiors.len()).expect("Couldn't cast `interiors.len()` to `u32`"), + ); + + for vertex in hole { + interiors.push(vertex.coordinate.x); + interiors.push(vertex.coordinate.y); + } + } + + Self { + exterior, + interiors, + hole_indices, + } + } +} + +/// Reconstruct a viewshed from raw polar segments. +/// +/// # Panics +/// When reconstructing the viewshed fails. +#[expect( + unreachable_pub, + reason = "We won't it to be `pub` for WASM but not avaiable outside the crate" +)] +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen] +#[must_use] +pub fn reconstruct(js_data: &js_sys::Array, dem_scale: f32) -> js_sys::Array { + console_error_panic_hook::set_once(); + let mut rust_data: Vec> = Vec::new(); + + for segments_for_angle in js_data.iter() { + let inner_js_array: js_sys::Array = segments_for_angle.unchecked_into(); + let mut inner_rust_vec = Vec::new(); + + for segment in inner_js_array.iter() { + #[expect( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + clippy::as_conversions, + reason = " + We're only worried about the truncation here, but it's very unlikely. + Remember that this value is actually a bitpack of 2 `u16`s. + " + )] + let bitpack = segment + .as_f64() + .expect("Expected valid JavaScript `number`") as u32; + + inner_rust_vec.push(crate::segment::Segment(bitpack)); + } + rust_data.push(inner_rust_vec); + } + + let growable_polygons = crate::joiner::Joiner::join(&rust_data, dem_scale); + + let js_array = Array::new(); + for growable_polygon in growable_polygons { + let plain_polygon = PlainPolygon::new(&growable_polygon); + js_array.push(&JsValue::from(plain_polygon)); + } + js_array +}