diff --git a/.vscode/settings.json b/.vscode/settings.json index 608e2d6..9404440 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,6 @@ "rust-analyzer.cargo.target": "i686-pc-windows-msvc", "rust-analyzer.linkedProjects": [ ".\\Cargo.toml" - ] + ], + "rust-analyzer.showUnlinkedFileNotification": false } diff --git a/Cargo.lock b/Cargo.lock index 48e3c6d..b1acb98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -569,6 +569,15 @@ dependencies = [ "adler32", ] +[[package]] +name = "delaunator" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab46e386c7a38300a0d93b0f3e484bc2ee0aded66c47b14762ec9ab383934fa" +dependencies = [ + "robust", +] + [[package]] name = "deflate64" version = "0.1.10" @@ -2899,6 +2908,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "robust" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5864e7ef1a6b7bcf1d6ca3f655e65e724ed3b52546a0d0a663c991522f552ea" + [[package]] name = "rust-g" version = "6.0.0" @@ -2913,6 +2928,7 @@ dependencies = [ "cuid2", "dashmap", "dbpnoise", + "delaunator", "dmi", "fast_poisson", "flume", diff --git a/Cargo.toml b/Cargo.toml index a10be86..7c5721c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,8 @@ dbpnoise = { version = "0.1.2", optional = true } pathfinding = { version = "4.14", optional = true } num-integer = { version = "0.1.46", optional = true } dmi = { version = "0.5.0", optional = true } +delaunator = { version = "1.0.2", optional = true } +voronoice = { version = "0.2.0", optional = true } tracy_full = { version = "1.12.0", optional = true } ammonia = { version = "4.1", optional = true } fast_poisson = { version = "1.0.2", optional = true, features = [ @@ -84,12 +86,14 @@ ordered-float = { version = "5.1.0", optional = true, features = ["serde"] } qrcode = { version = "0.14.1", optional = true, features = ["image", "svg"]} [features] +# These are things that Citadel needs to run. default = [ "acreplace", "batchnoise", "cellularnoise", "dmi", "file", + "geometry", "git", "hash", "http", @@ -108,6 +112,7 @@ default = [ "url", ] +# These are everything in the library, including /tg/station code. all = [ "acreplace", "batchnoise", @@ -115,6 +120,7 @@ all = [ "dmi", "dice", "file", + "geometry", "git", "hash", "http", @@ -144,6 +150,7 @@ batchnoise = ["dbpnoise"] cellularnoise = ["rand", "rayon"] dmi = ["png", "image", "qrcode", "serde_repr"] file = [] +geometry = ["delaunator", "voronoice"] git = ["gix", "chrono"] hash = [ "base32", diff --git a/dmsrc/geometry.dm b/dmsrc/geometry.dm new file mode 100644 index 0000000..c62f23b --- /dev/null +++ b/dmsrc/geometry.dm @@ -0,0 +1,9 @@ +/** + * Please see code/datums/math/vec2.dm. + */ +#define rustg_geometry_delaunay_triangulate_to_graph(point_json) RUSTG_CALL(RUST_G, "geometry_delaunay_triangulate_to_graph")(point_json) + +/** + * Please see code/datums/math/vec2.dm. + */ +#define rustg_geometry_delaunay_voronoi_graph(packed) RUSTG_CALL(RUST_G, "geometry_delaunay_voronoi_graph")(packed) diff --git a/src/geometry.rs b/src/geometry.rs new file mode 100644 index 0000000..d4f83fc --- /dev/null +++ b/src/geometry.rs @@ -0,0 +1,191 @@ +use delaunator::Point; +use serde::{Deserialize, Serialize}; +use voronoice::BoundingBox; + +/** + * This file is tightly coupled with Citadel Station's repository. + * + * Currently bound files: + * + * code/datums/math/vec2.dm + * code/datums/math/graph.dm + * code/datums/math/digraph.dm + */ + +#[derive(Serialize, Deserialize, Clone)] +struct DMVec2 { + x: f64, + y: f64, + area: Option, + cell: Option>, +} + +impl DMVec2 { + /** + * input vertices must be specified clockwise! + */ + pub fn polygon_area(vertices: &[DMVec2]) -> f64 { + let size = vertices.len(); + let mut area: f64 = 0_f64; + for i in 0..size { + let j = (i + 1) % size; + area += vertices[i].x * vertices[j].y; + area -= vertices[i].y * vertices[j].x; + } + -area + } +} + +/** + * count is the number of vertices + * edges are indexed, and are a list of indices an index is connected to. + */ +#[derive(Serialize, Deserialize, Clone)] +struct DMGraph { + count: usize, + edges: Vec>, +} + +impl DMGraph { + pub fn empty_of_size(size: usize) -> DMGraph { + let mut building = DMGraph { + count: size, + edges: Vec::new(), + }; + building.edges = vec![Vec::new(); size]; + building + } + + pub fn connect(&mut self, a: usize, b: usize) { + self.connect_single(a, b); + self.connect_single(b, a); + } + + pub fn connect_single(&mut self, a: usize, b: usize) { + let edge_list = &mut self.edges[a]; + if edge_list.iter().any(|&e| e == b) { + return; + } + edge_list.push(b); + } +} + +byond_fn!( + fn geometry_delaunay_triangulate_to_graph(point_json) { + let points: Vec = match serde_json::from_str(point_json) { + Ok(r) => r, + Err(_) => return Some("error during json decode".to_string()), + }; + let transmuted: Vec = points.iter().map(|p| Point{x: p.x, y: p.y}).collect(); + let triangulated = delaunator::triangulate(&transmuted); + let mut constructing = DMGraph::empty_of_size(points.len()); + for chunk in triangulated.triangles.chunks_exact(3) { + let a = chunk[0]; + let b = chunk[1]; + let c = chunk[2]; + constructing.connect(a, b); + constructing.connect(a, c); + constructing.connect(b, c); + }; + let encoded = serde_json::to_string(&constructing); + match encoded { + Ok(json) => Some(json), + Err(nope) => Some(nope.to_string()), + } + } +); + +/** + * call data + */ +#[derive(Deserialize)] +struct DMDelaunayVoronoiCall { + area: f64, + cell: f64, + margin: f64, + points: Vec, +} + +/** + * call return + */ +#[derive(Serialize)] +struct DMDelaunayVoronoiReturn { + graph: DMGraph, + areas: Vec>, + cells: Vec>>, +} + +byond_fn!( + fn geometry_delaunay_voronoi_graph(packed) { + let unpacked: DMDelaunayVoronoiCall = match serde_json::from_str(packed) { + Ok(r) => r, + Err(_) => return Some("error during json decode".to_string()), + }; + let transmuted: Vec = unpacked.points.iter().map(|p| Point{x: p.x, y: p.y}).collect(); + let mut x_low: f64 = f64::INFINITY; + let mut x_high: f64 = -f64::INFINITY; + let mut y_low: f64 = f64::INFINITY; + let mut y_high: f64 = -f64::INFINITY; + let margin = unpacked.margin; + for point in transmuted.iter() { + x_low = x_low.min(point.x); + x_high = x_high.max(point.x); + y_low = y_low.min(point.y); + y_high = y_high.max(point.y); + } + let center_point = Point{x: x_low + (x_high - x_low) * 0.5, y: y_low + (y_high - y_low) * 0.5}; + let requires_area = unpacked.area != 0_f64; + let requires_cell = unpacked.cell != 0_f64; + let computed = match voronoice::VoronoiBuilder::default() + .set_sites(transmuted) + .set_bounding_box( + BoundingBox::new(center_point, (x_high - x_low) + margin * 2_f64, (y_high - y_low) + margin * 2_f64) + ) + .build() { + Some(c) => c, + None => return Some("error during voronoi solve".to_string()), + }; + let count = unpacked.points.len(); + let mut constructing_graph = DMGraph::empty_of_size(count); + for chunk in computed.triangulation().triangles.chunks_exact(3) { + let a = chunk[0]; + let b = chunk[1]; + let c = chunk[2]; + constructing_graph.connect(a.to_owned(), b.to_owned()); + constructing_graph.connect(a.to_owned(), c.to_owned()); + constructing_graph.connect(b.to_owned(), c.to_owned()); + }; + let mut areas_constructed: Vec> = vec![Option::None; count]; + let mut cells_constructed: Vec>> = vec![Option::None; count]; + for i in 0..count { + let cell = computed.cell(i); + let mut vertices_constructed: Vec = Vec::new(); + for vertex in cell.iter_vertices() { + vertices_constructed.push( + DMVec2{ + x: vertex.x, + y: vertex.y, + area: Option::None, + cell: Option::None, + } + ); + } + if requires_area { + areas_constructed[i] = Some(DMVec2::polygon_area(&vertices_constructed)); + } + if requires_cell { + cells_constructed[i] = Some(vertices_constructed); + } + } + let encoded = serde_json::to_string(&DMDelaunayVoronoiReturn{ + graph: constructing_graph, + areas: areas_constructed, + cells: cells_constructed, + }); + match encoded { + Ok(r) => Some(r), + Err(_) => Some("error during json encode".to_string()), + } + } +); diff --git a/src/lib.rs b/src/lib.rs index f915c5c..8495fab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,6 +24,8 @@ pub mod dice; pub mod dmi; #[cfg(feature = "file")] pub mod file; +#[cfg(feature = "geometry")] +pub mod geometry; #[cfg(feature = "git")] pub mod git; #[cfg(feature = "hash")]