aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRuben Beltran del Rio <jj@r.bdr.sh>2025-12-16 12:07:43 +0100
committerRuben Beltran del Rio <jj@r.bdr.sh>2025-12-16 16:34:52 +0100
commitafd0794104228cd20e177f41dc68e9ae7ecaabda (patch)
treea7ee69438b0906b03b4b4a88fd376023a96acce0
parent2ae9158285a22c023ebd0f88bbadd8b0832079ea (diff)
Performance improvements, prep for publish.
-rw-r--r--Cargo.lock351
-rw-r--r--Cargo.toml36
-rw-r--r--README.md52
-rw-r--r--benches/rendering.rs43
-rw-r--r--examples/generate_map.rs2
-rw-r--r--src/configuration.rs152
-rw-r--r--src/error.rs31
-rw-r--r--src/geometry.rs102
-rw-r--r--src/lib.rs102
-rw-r--r--src/patterns.rs37
-rw-r--r--src/renderer.rs773
-rw-r--r--src/shapes.rs97
-rw-r--r--src/smart_positioning.rs85
-rw-r--r--src/stage_type.rs2
-rw-r--r--src/utils.rs35
-rw-r--r--tests/integration_test.rs2
16 files changed, 1044 insertions, 858 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 4ea8935..8f06b42 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -36,6 +36,15 @@ dependencies = [
]
[[package]]
+name = "alloca"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4"
+dependencies = [
+ "cc",
+]
+
+[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -121,7 +130,7 @@ dependencies = [
"num-traits",
"pastey",
"rayon",
- "thiserror 2.0.17",
+ "thiserror",
"v_frame",
"y4m",
]
@@ -217,9 +226,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "cairo-rs"
-version = "0.20.12"
+version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91e3bd0f4e25afa9cabc157908d14eeef9067d6448c49414d17b3fb55f0eadd0"
+checksum = "b01fe135c0bd16afe262b6dea349bd5ea30e6de50708cec639aae7c5c14cc7e4"
dependencies = [
"bitflags",
"cairo-sys-rs",
@@ -229,9 +238,9 @@ dependencies = [
[[package]]
name = "cairo-sys-rs"
-version = "0.20.10"
+version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "059cc746549898cbfd9a47754288e5a958756650ef4652bbb6c5f71a6bda4f8b"
+checksum = "06c28280c6b12055b5e39e4554271ae4e6630b27c0da9148c4cf6485fc6d245c"
dependencies = [
"glib-sys",
"libc",
@@ -350,25 +359,24 @@ dependencies = [
[[package]]
name = "criterion"
-version = "0.5.1"
+version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
+checksum = "4d883447757bb0ee46f233e9dc22eb84d93a9508c9b868687b274fc431d886bf"
dependencies = [
+ "alloca",
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot",
- "is-terminal",
- "itertools 0.10.5",
+ "itertools 0.13.0",
"num-traits",
- "once_cell",
"oorandom",
+ "page_size",
"plotters",
"rayon",
"regex",
"serde",
- "serde_derive",
"serde_json",
"tinytemplate",
"walkdir",
@@ -376,12 +384,12 @@ dependencies = [
[[package]]
name = "criterion-plot"
-version = "0.5.0"
+version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
+checksum = "ed943f81ea2faa8dcecbbfa50164acf95d555afec96a27871663b300e387b2e4"
dependencies = [
"cast",
- "itertools 0.10.5",
+ "itertools 0.13.0",
]
[[package]]
@@ -464,7 +472,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
- "windows-sys 0.61.2",
+ "windows-sys",
]
[[package]]
@@ -610,18 +618,21 @@ dependencies = [
[[package]]
name = "geo"
-version = "0.28.0"
+version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f811f663912a69249fa620dcd2a005db7254529da2d8a0b23942e81f47084501"
+checksum = "3f3901269ec6d4f6068d3f09e5f02f995bd076398dcd1dfec407cd230b02d11b"
dependencies = [
"earcutr",
"float_next_after",
"geo-types",
"geographiclib-rs",
+ "i_overlay",
"log",
"num-traits",
+ "rand 0.8.5",
"robust",
"rstar",
+ "sif-itree",
"spade",
]
@@ -633,6 +644,7 @@ checksum = "24f8647af4005fa11da47cd56252c6ef030be8fa97bdbf355e7dfb6348f0a82c"
dependencies = [
"approx",
"num-traits",
+ "rayon",
"rstar",
"serde",
]
@@ -648,6 +660,17 @@ dependencies = [
[[package]]
name = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
@@ -670,22 +693,22 @@ dependencies = [
[[package]]
name = "gio-sys"
-version = "0.20.10"
+version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "521e93a7e56fc89e84aea9a52cfc9436816a4b363b030260b699950ff1336c83"
+checksum = "0071fe88dba8e40086c8ff9bbb62622999f49628344b1d1bf490a48a29d80f22"
dependencies = [
"glib-sys",
"gobject-sys",
"libc",
"system-deps",
- "windows-sys 0.59.0",
+ "windows-sys",
]
[[package]]
name = "glib"
-version = "0.20.12"
+version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ffc4b6e352d4716d84d7dde562dd9aee2a7d48beb872dd9ece7f2d1515b2d683"
+checksum = "16de123c2e6c90ce3b573b7330de19be649080ec612033d397d72da265f1bd8b"
dependencies = [
"bitflags",
"futures-channel",
@@ -704,9 +727,9 @@ dependencies = [
[[package]]
name = "glib-macros"
-version = "0.20.12"
+version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8084af62f09475a3f529b1629c10c429d7600ee1398ae12dd3bf175d74e7145"
+checksum = "cf59b675301228a696fe01c3073974643365080a76cc3ed5bc2cbc466ad87f17"
dependencies = [
"heck",
"proc-macro-crate",
@@ -717,9 +740,9 @@ dependencies = [
[[package]]
name = "glib-sys"
-version = "0.20.10"
+version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ab79e1ed126803a8fb827e3de0e2ff95191912b8db65cee467edb56fc4cc215"
+checksum = "2d95e1a3a19ae464a7286e14af9a90683c64d70c02532d88d87ce95056af3e6c"
dependencies = [
"libc",
"system-deps",
@@ -727,9 +750,9 @@ dependencies = [
[[package]]
name = "gobject-sys"
-version = "0.20.10"
+version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec9aca94bb73989e3cfdbf8f2e0f1f6da04db4d291c431f444838925c4c63eda"
+checksum = "2dca35da0d19a18f4575f3cb99fe1c9e029a2941af5662f326f738a21edaf294"
dependencies = [
"glib-sys",
"libc",
@@ -790,10 +813,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
-name = "hermit-abi"
-version = "0.5.2"
+name = "i_float"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "010025c2c532c8d82e42d0b8bb5184afa449fa6f06c709ea9adcb16c49ae405b"
+dependencies = [
+ "libm",
+]
+
+[[package]]
+name = "i_key_sort"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+checksum = "9190f86706ca38ac8add223b2aed8b1330002b5cdbbce28fb58b10914d38fc27"
+
+[[package]]
+name = "i_overlay"
+version = "4.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fcccbd4e4274e0f80697f5fbc6540fdac533cce02f2081b328e68629cce24f9"
+dependencies = [
+ "i_float",
+ "i_key_sort",
+ "i_shape",
+ "i_tree",
+ "rayon",
+]
+
+[[package]]
+name = "i_shape"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ea154b742f7d43dae2897fcd5ead86bc7b5eefcedd305a7ebf9f69d44d61082"
+dependencies = [
+ "i_float",
+]
+
+[[package]]
+name = "i_tree"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35e6d558e6d4c7b82bc51d9c771e7a927862a161a7d87bf2b0541450e0e20915"
[[package]]
name = "image"
@@ -857,30 +917,19 @@ dependencies = [
]
[[package]]
-name = "is-terminal"
-version = "0.4.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
-dependencies = [
- "hermit-abi",
- "libc",
- "windows-sys 0.61.2",
-]
-
-[[package]]
name = "itertools"
-version = "0.10.5"
+version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
dependencies = [
"either",
]
[[package]]
name = "itertools"
-version = "0.11.0"
+version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
@@ -906,7 +955,7 @@ version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
- "getrandom",
+ "getrandom 0.3.4",
"libc",
]
@@ -1090,6 +1139,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
[[package]]
+name = "page_size"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1216,8 +1275,8 @@ dependencies = [
"bit-vec",
"bitflags",
"num-traits",
- "rand",
- "rand_chacha",
+ "rand 0.9.2",
+ "rand_chacha 0.9.0",
"rand_xorshift",
"regex-syntax",
"rusty-fork",
@@ -1272,12 +1331,33 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
- "rand_chacha",
- "rand_core",
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.3",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
]
[[package]]
@@ -1287,7 +1367,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
- "rand_core",
+ "rand_core 0.9.3",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.16",
]
[[package]]
@@ -1296,7 +1385,7 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
- "getrandom",
+ "getrandom 0.3.4",
]
[[package]]
@@ -1305,7 +1394,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a"
dependencies = [
- "rand_core",
+ "rand_core 0.9.3",
]
[[package]]
@@ -1335,10 +1424,10 @@ dependencies = [
"num-traits",
"paste",
"profiling",
- "rand",
- "rand_chacha",
+ "rand 0.9.2",
+ "rand_chacha 0.9.0",
"simd_helpers",
- "thiserror 2.0.17",
+ "thiserror",
"v_frame",
"wasm-bindgen",
]
@@ -1431,6 +1520,12 @@ dependencies = [
]
[[package]]
+name = "rustc-hash"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+
+[[package]]
name = "rustix"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1440,7 +1535,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
- "windows-sys 0.61.2",
+ "windows-sys",
]
[[package]]
@@ -1535,6 +1630,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
+name = "sif-itree"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "142099cd6db3c4fab61e5133c62ff80b26674391e195860791fda0b1be3e5080"
+
+[[package]]
name = "simd-adler32"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1616,19 +1717,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
dependencies = [
"fastrand",
- "getrandom",
+ "getrandom 0.3.4",
"once_cell",
"rustix",
- "windows-sys 0.61.2",
-]
-
-[[package]]
-name = "thiserror"
-version = "1.0.69"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
-dependencies = [
- "thiserror-impl 1.0.69",
+ "windows-sys",
]
[[package]]
@@ -1637,18 +1729,7 @@ version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
dependencies = [
- "thiserror-impl 2.0.17",
-]
-
-[[package]]
-name = "thiserror-impl"
-version = "1.0.69"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "thiserror-impl",
]
[[package]]
@@ -1786,6 +1867,12 @@ dependencies = [
]
[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
name = "wasip2"
version = "1.0.1+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1856,28 +1943,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
- "windows-sys 0.61.2",
+ "windows-sys",
]
[[package]]
-name = "windows-link"
-version = "0.2.1"
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
-name = "windows-sys"
-version = "0.59.0"
+name = "windows-link"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
-dependencies = [
- "windows-targets",
-]
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
@@ -1889,70 +1989,6 @@ dependencies = [
]
[[package]]
-name = "windows-targets"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
-dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_gnullvm",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
-]
-
-[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
-
-[[package]]
-name = "windows_i686_gnu"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
-
-[[package]]
-name = "windows_i686_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
-
-[[package]]
-name = "windows_i686_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
-
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
-
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
-
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
-
-[[package]]
name = "winnow"
version = "0.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1970,6 +2006,8 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "wmap-parser"
version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5a2c331c28b392f06cd9d81118a6f73b084708c6ff320457f4cbd75912d499b"
[[package]]
name = "wmap-renderer"
@@ -1981,8 +2019,9 @@ dependencies = [
"image",
"once_cell",
"proptest",
+ "rustc-hash",
"tempfile",
- "thiserror 1.0.69",
+ "thiserror",
"wmap-parser",
]
diff --git a/Cargo.toml b/Cargo.toml
index 0bc362d..1b5a49d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,29 +1,24 @@
[package]
name = "wmap-renderer"
version = "0.1.0"
-edition = "2021"
+edition = "2024"
+license = "AGPL-3.0-or-later"
+description = "A parser for wmap formatted Wardley Map files."
+homepage = "https://git.sr.ht/~rbdr/wmap-parser-rust"
+authors = ["Rubén Beltrán del Río <wmap.parser@r.bdr.sh>"]
[dependencies]
-# Core rendering
-cairo-rs = { version = "0.20", features = ["png", "svg"] }
-
-# Local dependency (not published)
-wmap-parser = { path = "../wmap-parser-rust" }
-
-# Geometry
-geo = "0.28"
-
-# Error handling
-thiserror = "1.0"
-
-# Utilities
-once_cell = "1.19"
-tempfile = "3.15"
+cairo-rs = { version = "0.21", features = ["png", "svg"] }
+geo = "0.32"
+thiserror = "2.0"
+wmap-parser = "1.0"
+once_cell = "1.21"
+tempfile = "3.23"
+rustc-hash = "2.1"
[dev-dependencies]
-# Testing
-criterion = "0.5"
-proptest = "1.4"
+criterion = "0.8"
+proptest = "1.9"
image = "0.25"
[[bench]]
@@ -34,3 +29,6 @@ harness = false
opt-level = 3
lto = true
codegen-units = 1
+
+[lints.clippy]
+pedantic = "warn"
diff --git a/README.md b/README.md
index 98e45f2..889b75e 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,20 @@
-# Wardley Map Renderer (Rust)
+# wmap-renderer
-A high-performance Wardley map renderer written in Rust, providing complete feature parity with the JavaScript/Canvas implementation.
+A rust renderer for `wmap` formatted wardley maps parsed with [`wmap-parser`][wmap-parser-rust].
## Features
-- ✅ **Multiple Output Formats**: PNG and SVG support via cairo-rs
-- ✅ **All Rendering Features**:
- - Axes with customizable stage labels
- - Components with 4 shape types (circle, square, triangle, x)
- - Dependencies with optional arrowheads
- - Evolution arrows with bidirectional support
- - Inertia indicators
- - Notes with multiline text support
- - Groups with concave hull rendering
- - Background evolution stage patterns
-- ✅ **21 Stage Types**: Activities, Behavior, Certainty, Comparison, Cynefin, Data, Decision Drivers, Efficiency, Failure, Focus of Value, Knowledge, Knowledge Management, Market, Market Action, Market Perception, Perception in Industry, Practice, Publication Types, Ubiquity, Understanding, User Perception, Evolution Stage
-- ✅ **Complete Configuration**: Colors, sizes, fonts, line heights, and opacity all configurable
-- ✅ **Visual Parity**: Matches JavaScript implementation output exactly
+- Can render to PNG or SVG.
+- Rendering logic attempts to replicate [map][map]'s output.
+
+## Installation
+
+Add it to your project with cargo add
+
+```bash
+cargo add wmap-renderer
+```
+
## Usage
@@ -77,11 +75,18 @@ All 22 tests should pass (17 unit tests + 5 integration tests).
## Performance
-The Rust implementation is significantly faster than the JavaScript version:
-- Example map (~4 components): < 5ms
-- Large maps (100+ components): ~10-20ms
-- No garbage collection pauses
-- Efficient memory usage
+The drawing is not optimized for live-editing. It's relatively quick to
+generate a single image. On an M1 Pro mac, our xample map renders in about ~2ms,
+while a larger map with slightly under 2000 entities does so in ~140ms.
+
+Turning off smart labels doesn't have a big effect on medium to small maps,
+but in our large map test it renders in ~97ms instead.
+
+You can run the benchmarks by using:
+
+```bash
+make benchmark
+```
## Dependencies
@@ -104,11 +109,8 @@ The Rust implementation is significantly faster than the JavaScript version:
- ✅ Comprehensive test suite
- ✅ Example programs
-### Future Optimizations (Optional)
-- ⏱ Smart label positioning algorithm (currently uses simple right-side positioning)
-- ⏱ Parallel rendering for large maps
-- ⏱ PDF output support
-
## License
See LICENSE file.
+[wmap-parser-rust]: https://crates.io/crates/wmap-parser
+[map]: https://map.tranquil.systems
diff --git a/benches/rendering.rs b/benches/rendering.rs
index 51e1b13..7fe7877 100644
--- a/benches/rendering.rs
+++ b/benches/rendering.rs
@@ -1,22 +1,51 @@
-use criterion::{black_box, criterion_group, criterion_main, Criterion};
+use criterion::{Criterion, black_box, criterion_group, criterion_main};
use std::fs;
-use wmap_renderer::{render_to_surface, Configuration, StageType};
+use wmap_renderer::{Configuration, StageType, render_to_surface};
fn benchmark_rendering(c: &mut Criterion) {
- let config = Configuration::default();
+ let configuration = Configuration::default();
- let example_source = fs::read_to_string("benches/example.wmap").expect("Failed to read benches/example.wmap");
+ let mut configuration_no_smart = Configuration::default();
+ configuration_no_smart.options.smart_label_positioning = false;
+
+ let example_source =
+ fs::read_to_string("benches/example.wmap").expect("Failed to read benches/example.wmap");
let example_map = wmap_parser::parse(&example_source);
c.bench_function("render example map", |b| {
- b.iter(|| render_to_surface(black_box(&example_map), StageType::default(), &config))
+ b.iter(|| {
+ render_to_surface(
+ black_box(&example_map),
+ StageType::default(),
+ &configuration,
+ )
+ })
+ });
+ c.bench_function("render example map (no smart labels))", |b| {
+ b.iter(|| {
+ render_to_surface(
+ black_box(&example_map),
+ StageType::default(),
+ &configuration_no_smart,
+ )
+ })
});
- let huge_source = fs::read_to_string("benches/huge.wmap").expect("Failed to read benches/huge.wmap");
+ let huge_source =
+ fs::read_to_string("benches/huge.wmap").expect("Failed to read benches/huge.wmap");
let huge_map = wmap_parser::parse(&huge_source);
c.bench_function("render huge map", |b| {
- b.iter(|| render_to_surface(black_box(&huge_map), StageType::default(), &config))
+ b.iter(|| render_to_surface(black_box(&huge_map), StageType::default(), &configuration))
+ });
+ c.bench_function("render huge map", |b| {
+ b.iter(|| {
+ render_to_surface(
+ black_box(&huge_map),
+ StageType::default(),
+ &configuration_no_smart,
+ )
+ })
});
}
diff --git a/examples/generate_map.rs b/examples/generate_map.rs
index 8b9cf80..2d83b0b 100644
--- a/examples/generate_map.rs
+++ b/examples/generate_map.rs
@@ -1,5 +1,5 @@
use std::fs;
-use wmap_renderer::{render_to_png, render_to_svg, Configuration, StageType};
+use wmap_renderer::{Configuration, StageType, render_to_png, render_to_svg};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Read the example.wmap file
diff --git a/src/configuration.rs b/src/configuration.rs
index 775d6cd..bc99781 100644
--- a/src/configuration.rs
+++ b/src/configuration.rs
@@ -17,34 +17,34 @@ impl Default for Options {
/// Color configuration
#[derive(Debug, Clone)]
pub struct Colors {
- pub background: String,
- pub axis: String,
- pub vertex: String,
- pub label: String,
- pub stage_foreground: String,
- pub stage_background: String,
- pub inertia: String,
- pub evolution: String,
- pub groups: Vec<String>,
+ pub background: (f64, f64, f64),
+ pub axis: (f64, f64, f64),
+ pub vertex: (f64, f64, f64),
+ pub label: (f64, f64, f64),
+ pub stage_foreground: (f64, f64, f64),
+ pub stage_background: (f64, f64, f64),
+ pub inertia: (f64, f64, f64),
+ pub evolution: (f64, f64, f64),
+ pub groups: Vec<(f64, f64, f64)>,
}
impl Default for Colors {
fn default() -> Self {
Self {
- background: "#FFFFFF".to_string(),
- axis: "#0F261F".to_string(),
- vertex: "#0F261F".to_string(),
- label: "#0F261F".to_string(),
- stage_foreground: "#DAE6E3".to_string(),
- stage_background: "#FFFFFF".to_string(),
- inertia: "#FA2B00".to_string(),
- evolution: "#4F8FE6".to_string(),
+ background: (1.0, 1.0, 1.0),
+ axis: (0.058824, 0.149020, 0.121569), // Deep Slate Green
+ vertex: (0.058824, 0.149020, 0.121569),
+ label: (0.058824, 0.149020, 0.121569),
+ stage_foreground: (0.854901, 0.901960, 0.890196),
+ stage_background: (1.0, 1.0, 1.0),
+ inertia: (0.980392, 0.168627, 0.0),
+ evolution: (0.309804, 0.560784, 0.901961),
groups: vec![
- "#3A5BA0".to_string(), // Olympic Blue
- "#C44536".to_string(), // Jasper Red
- "#8FBE92".to_string(), // Light Porcelain Green
- "#FAD55C".to_string(), // Naples Yellow
- "#EC9CAB".to_string(), // Hermosa Pink
+ (0.227451, 0.356863, 0.627451), // Olympic Blue
+ (0.768627, 0.270588, 0.211765), // Jasper Red
+ (0.560784, 0.745098, 0.572549), // Light Porcelain Green
+ (0.980392, 0.835294, 0.360784), // Naples Yellow
+ (0.925490, 0.611765, 0.670588), // Hermosa Pink
],
}
}
@@ -81,7 +81,7 @@ impl Default for Sizes {
/// Font configuration
#[derive(Debug, Clone)]
pub struct Fonts {
- pub family: String,
+ pub face: String,
pub axis_label: f64,
pub vertex_label: f64,
pub note: f64,
@@ -90,7 +90,7 @@ pub struct Fonts {
impl Default for Fonts {
fn default() -> Self {
Self {
- family: "Helvetica Neue, Helvetica, Arial, sans-serif".to_string(),
+ face: "sans-serif".to_string(),
axis_label: 14.0,
vertex_label: 12.0,
note: 12.0,
@@ -149,47 +149,89 @@ mod tests {
#[test]
fn test_default_configuration() {
- let config = Configuration::default();
+ let configuration = Configuration::default();
+ const ALLOWED_ERROR_VEHICLE_LENGTH_CM: f64 = 0.1;
// Test options
- assert!(config.options.show_background);
- assert!(config.options.smart_label_positioning);
+ assert!(configuration.options.show_background);
+ assert!(configuration.options.smart_label_positioning);
- // Test colors (matching JS implementation exactly)
- assert_eq!(config.theme.colors.background, "#FFFFFF");
- assert_eq!(config.theme.colors.axis, "#0F261F");
- assert_eq!(config.theme.colors.vertex, "#0F261F");
- assert_eq!(config.theme.colors.label, "#0F261F");
- assert_eq!(config.theme.colors.stage_foreground, "#DAE6E3");
- assert_eq!(config.theme.colors.stage_background, "#FFFFFF");
- assert_eq!(config.theme.colors.inertia, "#FA2B00");
- assert_eq!(config.theme.colors.evolution, "#4F8FE6");
- assert_eq!(config.theme.colors.groups.len(), 5);
+ // Test colors
+ assert_eq!(configuration.theme.colors.background, (1.0, 1.0, 1.0));
+ assert_eq!(
+ configuration.theme.colors.axis,
+ (0.058_824, 0.149_020, 0.121_569)
+ );
+ assert_eq!(
+ configuration.theme.colors.vertex,
+ (0.058_824, 0.149_020, 0.121_569)
+ );
+ assert_eq!(
+ configuration.theme.colors.label,
+ (0.058_824, 0.149_020, 0.121_569)
+ );
+ assert_eq!(
+ configuration.theme.colors.stage_foreground,
+ (0.854_901, 0.901_960, 0.890_196)
+ );
+ assert_eq!(configuration.theme.colors.stage_background, (1.0, 1.0, 1.0));
+ assert_eq!(
+ configuration.theme.colors.inertia,
+ (0.980_392, 0.168_627, 0.0)
+ );
+ assert_eq!(
+ configuration.theme.colors.evolution,
+ (0.309_804, 0.560_784, 0.901_961)
+ );
+ assert_eq!(configuration.theme.colors.groups.len(), 5);
- // Test sizes (matching JS implementation with extra bottom padding for wrapped labels)
- assert_eq!(config.theme.sizes.map_width, 1300.0);
- assert_eq!(config.theme.sizes.map_height, 1000.0);
- assert_eq!(config.theme.sizes.padding, 42.0);
- assert_eq!(config.theme.sizes.bottom_padding, 120.0);
- assert_eq!(config.theme.sizes.line_width, 0.5);
- assert_eq!(config.theme.sizes.vertex_size, 25.0);
- assert_eq!(config.theme.sizes.arrowhead_size, 10.0);
- assert_eq!(config.theme.sizes.stage_height, 100.0);
+ // Test sizes
+ assert!(
+ (configuration.theme.sizes.map_width - 1300.0).abs() < ALLOWED_ERROR_VEHICLE_LENGTH_CM
+ );
+ assert!(
+ (configuration.theme.sizes.map_height - 1000.0).abs() < ALLOWED_ERROR_VEHICLE_LENGTH_CM
+ );
+ assert!((configuration.theme.sizes.padding - 42.0).abs() < ALLOWED_ERROR_VEHICLE_LENGTH_CM);
+ assert!(
+ (configuration.theme.sizes.bottom_padding - 120.0).abs()
+ < ALLOWED_ERROR_VEHICLE_LENGTH_CM
+ );
+ assert!(
+ (configuration.theme.sizes.line_width - 0.5).abs() < ALLOWED_ERROR_VEHICLE_LENGTH_CM
+ );
+ assert!(
+ (configuration.theme.sizes.vertex_size - 25.0).abs() < ALLOWED_ERROR_VEHICLE_LENGTH_CM
+ );
+ assert!(
+ (configuration.theme.sizes.arrowhead_size - 10.0).abs()
+ < ALLOWED_ERROR_VEHICLE_LENGTH_CM
+ );
+ assert!(
+ (configuration.theme.sizes.stage_height - 100.0).abs()
+ < ALLOWED_ERROR_VEHICLE_LENGTH_CM
+ );
// Test fonts
- assert_eq!(
- config.theme.fonts.family,
- "Helvetica Neue, Helvetica, Arial, sans-serif"
+ assert_eq!(configuration.theme.fonts.face, "sans-serif");
+ assert!(
+ (configuration.theme.fonts.axis_label - 14.0).abs() < ALLOWED_ERROR_VEHICLE_LENGTH_CM
+ );
+ assert!(
+ (configuration.theme.fonts.vertex_label - 12.0).abs() < ALLOWED_ERROR_VEHICLE_LENGTH_CM
);
- assert_eq!(config.theme.fonts.axis_label, 14.0);
- assert_eq!(config.theme.fonts.vertex_label, 12.0);
- assert_eq!(config.theme.fonts.note, 12.0);
+ assert!((configuration.theme.fonts.note - 12.0).abs() < ALLOWED_ERROR_VEHICLE_LENGTH_CM);
// Test line heights
- assert_eq!(config.theme.line_heights.vertex_label, 18.0);
- assert_eq!(config.theme.line_heights.note, 18.0);
+ assert!(
+ (configuration.theme.line_heights.vertex_label - 18.0).abs()
+ < ALLOWED_ERROR_VEHICLE_LENGTH_CM
+ );
+ assert!(
+ (configuration.theme.line_heights.note - 18.0).abs() < ALLOWED_ERROR_VEHICLE_LENGTH_CM
+ );
// Test opacity
- assert_eq!(config.theme.opacity.groups, 0.1);
+ assert!((configuration.theme.opacity.groups - 0.1).abs() < ALLOWED_ERROR_VEHICLE_LENGTH_CM);
}
}
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..1d6790b
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,31 @@
+use cairo::{Error as CairoError, IoError};
+use std::io::Error as StdError;
+use thiserror::Error;
+
+/// Error types for rendering
+#[derive(Debug, Error)]
+pub enum RenderError {
+ #[error("Cairo rendering error: {0}")]
+ Cairo(String),
+
+ #[error("Invalid configuration: {0}")]
+ InvalidConfig(String),
+
+ #[error("Concave hull calculation failed")]
+ GeometryError,
+
+ #[error("IO error: {0}")]
+ Io(#[from] StdError),
+}
+
+impl From<CairoError> for RenderError {
+ fn from(err: CairoError) -> Self {
+ RenderError::Cairo(err.to_string())
+ }
+}
+
+impl From<IoError> for RenderError {
+ fn from(err: IoError) -> Self {
+ RenderError::Cairo(err.to_string())
+ }
+}
diff --git a/src/geometry.rs b/src/geometry.rs
index 5099ada..0841183 100644
--- a/src/geometry.rs
+++ b/src/geometry.rs
@@ -5,6 +5,12 @@ pub struct Line {
pub end: (f64, f64),
}
+impl Line {
+ pub fn new(start: (f64, f64), end: (f64, f64)) -> Self {
+ Self { start, end }
+ }
+}
+
/// Represents a rectangle
#[derive(Debug, Clone, Copy)]
pub struct Rect {
@@ -14,12 +20,6 @@ pub struct Rect {
pub height: f64,
}
-impl Line {
- pub fn new(start: (f64, f64), end: (f64, f64)) -> Self {
- Self { start, end }
- }
-}
-
impl Rect {
pub fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
Self {
@@ -71,28 +71,106 @@ pub fn line_intersects_line(l1: &Line, l2: &Line) -> bool {
/// Checks if a line segment intersects a rectangle
pub fn line_intersects_rect(line: &Line, rect: &Rect) -> bool {
- // Check if either endpoint is inside the rectangle
if point_in_rect(line.start, rect) || point_in_rect(line.end, rect) {
return true;
}
- // Check intersection with all four edges
let edges = [
- Line::new((rect.x, rect.y), (rect.x + rect.width, rect.y)), // Top
+ Line::new((rect.x, rect.y), (rect.x + rect.width, rect.y)),
Line::new(
(rect.x + rect.width, rect.y),
(rect.x + rect.width, rect.y + rect.height),
- ), // Right
+ ),
Line::new(
(rect.x + rect.width, rect.y + rect.height),
(rect.x, rect.y + rect.height),
- ), // Bottom
- Line::new((rect.x, rect.y + rect.height), (rect.x, rect.y)), // Left
+ ),
+ Line::new((rect.x, rect.y + rect.height), (rect.x, rect.y)),
];
edges.iter().any(|edge| line_intersects_line(line, edge))
}
+/// Spatial hash grid for fast collision detection
+/// that stores cells as indices of lines.
+pub struct SpatialGrid {
+ cell_size: f64,
+ cells: Vec<Vec<usize>>,
+ columns: usize,
+ rows: usize,
+}
+
+impl SpatialGrid {
+ /// Creates a new spatial grid for the given dimensions
+ pub fn new(width: f64, height: f64, cell_size: f64) -> Self {
+ let columns = ((width / cell_size).ceil() as usize).max(1);
+ let rows = ((height / cell_size).ceil() as usize).max(1);
+ let cells = vec![Vec::new(); columns * rows];
+
+ Self {
+ cell_size,
+ cells,
+ columns,
+ rows,
+ }
+ }
+
+ /// Inserts a line into the spatial grid
+ pub fn insert_line(&mut self, line_idx: usize, line: &Line) {
+ let min_x = line.start.0.min(line.end.0);
+ let max_x = line.start.0.max(line.end.0);
+ let min_y = line.start.1.min(line.end.1);
+ let max_y = line.start.1.max(line.end.1);
+
+ let start_column = ((min_x / self.cell_size).floor() as usize).min(self.columns - 1);
+ let end_column = ((max_x / self.cell_size).floor() as usize).min(self.columns - 1);
+ let start_row = ((min_y / self.cell_size).floor() as usize).min(self.rows - 1);
+ let end_row = ((max_y / self.cell_size).floor() as usize).min(self.rows - 1);
+
+ for row in start_row..=end_row {
+ for col in start_column..=end_column {
+ let cell_idx = row * self.columns + col;
+ self.cells[cell_idx].push(line_idx);
+ }
+ }
+ }
+
+ /// Queries the spatial grid for lines that might intersect with the given rectangle
+ /// Returns line indices without duplicates
+ pub fn query_rect(&self, rect: &Rect) -> impl Iterator<Item = usize> + '_ {
+ let min_x = rect.x;
+ let max_x = rect.x + rect.width;
+ let min_y = rect.y;
+ let max_y = rect.y + rect.height;
+
+ let start_column = ((min_x / self.cell_size).floor() as isize)
+ .max(0)
+ .min(self.columns as isize - 1) as usize;
+ let end_column = ((max_x / self.cell_size).floor() as isize)
+ .max(0)
+ .min(self.columns as isize - 1) as usize;
+ let start_row = ((min_y / self.cell_size).floor() as isize)
+ .max(0)
+ .min(self.rows as isize - 1) as usize;
+ let end_row = ((max_y / self.cell_size).floor() as isize)
+ .max(0)
+ .min(self.rows as isize - 1) as usize;
+
+ let mut line_indices = Vec::new();
+ for row in start_row..=end_row {
+ for column in start_column..=end_column {
+ let cell_idx = row * self.columns + column;
+ line_indices.extend_from_slice(&self.cells[cell_idx]);
+ }
+ }
+
+ line_indices.sort_unstable();
+ line_indices.dedup();
+
+ line_indices.into_iter()
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/src/lib.rs b/src/lib.rs
index de4146d..52d10aa 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,4 +1,5 @@
mod configuration;
+mod error;
mod geometry;
mod patterns;
mod renderer;
@@ -7,114 +8,21 @@ mod smart_positioning;
mod stage_type;
mod utils;
-// Re-export parser types
-pub use wmap_parser::{
- Component, Dependency, Evolution, Group, Inertia, Map, Note, Shape, Stage, StageData,
-};
-
-// Re-export our types
pub use configuration::{
Colors, Configuration, Fonts, LineHeights, Opacity, Options, Sizes, Theme,
};
+pub use renderer::{render_to_png, render_to_surface, render_to_svg};
pub use stage_type::StageType;
-use thiserror::Error;
-use cairo::ImageSurface;
-
-/// Error types for rendering
-#[derive(Debug, Error)]
-pub enum RenderError {
- #[error("Cairo rendering error: {0}")]
- Cairo(String),
-
- #[error("Invalid configuration: {0}")]
- InvalidConfig(String),
-
- #[error("Concave hull calculation failed")]
- GeometryError,
-
- #[error("IO error: {0}")]
- Io(#[from] std::io::Error),
-}
-
-impl From<cairo::Error> for RenderError {
- fn from(err: cairo::Error) -> Self {
- RenderError::Cairo(err.to_string())
- }
-}
-
-impl From<cairo::IoError> for RenderError {
- fn from(err: cairo::IoError) -> Self {
- RenderError::Cairo(err.to_string())
- }
-}
-
-/// Renders a Wardley map to PNG format
-///
-/// # Arguments
-///
-/// * `map` - The parsed Wardley map
-/// * `stage_type` - The stage type to use for axis labels
-/// * `config` - Configuration options
-///
-/// # Returns
-///
-/// A vector of bytes containing the PNG image data
-pub fn render_to_png(
- map: &Map,
- stage_type: StageType,
- config: &Configuration,
-) -> Result<Vec<u8>, RenderError> {
- renderer::render_to_png(map, stage_type, config)
-}
-
-/// Renders a Wardley map to SVG format
-///
-/// # Arguments
-///
-/// * `map` - The parsed Wardley map
-/// * `stage_type` - The stage type to use for axis labels
-/// * `config` - Configuration options
-///
-/// # Returns
-///
-/// A string containing the SVG data
-pub fn render_to_svg(
- map: &Map,
- stage_type: StageType,
- config: &Configuration,
-) -> Result<String, RenderError> {
- renderer::render_to_svg(map, stage_type, config)
-}
-
-/// Renders a Wardley map to a cairo context.
-///
-/// # Arguments
-///
-/// * `map` - The parsed Wardley map
-/// * `stage_type` - The stage type to use for axis labels
-/// * `config` - Configuration options
-///
-/// # Returns
-///
-/// A string containing the SVG data
-pub fn render_to_surface(
- map: &Map,
- stage_type: StageType,
- config: &Configuration,
-) -> Result<ImageSurface, RenderError> {
- renderer::render_to_surface(map, stage_type, config)
-}
-
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_configuration() {
- let config = Configuration::default();
- assert!(config.options.show_background);
- assert!(config.options.smart_label_positioning);
+ let configuration = Configuration::default();
+ assert!(configuration.options.show_background);
+ assert!(configuration.options.smart_label_positioning);
}
#[test]
diff --git a/src/patterns.rs b/src/patterns.rs
index 12c790a..254433f 100644
--- a/src/patterns.rs
+++ b/src/patterns.rs
@@ -1,7 +1,5 @@
-use crate::utils::parse_color;
+use cairo::{Context, Error, Extend, Format, ImageSurface, SurfacePattern};
-/// 8x8 1-bit pattern data
-/// 0 = foreground color, 1 = background color
pub const STITCH: [u8; 64] = [
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1,
@@ -25,30 +23,28 @@ pub const WICKER: [u8; 64] = [
/// Creates a cairo surface pattern from pattern data
pub fn create_pattern(
pattern_data: &[u8; 64],
- foreground: &str,
- background: &str,
-) -> Result<cairo::SurfacePattern, cairo::Error> {
- let surface = cairo::ImageSurface::create(cairo::Format::Rgb24, 8, 8)?;
+ foreground: (f64, f64, f64),
+ background: (f64, f64, f64),
+) -> Result<SurfacePattern, Error> {
+ let surface = ImageSurface::create(Format::Rgb24, 8, 8)?;
{
- let ctx = cairo::Context::new(&surface)?;
- let fg = parse_color(foreground);
- let bg = parse_color(background);
+ let context = Context::new(&surface)?;
for y in 0..8 {
for x in 0..8 {
let value = pattern_data[y * 8 + x];
- let (r, g, b) = if value == 0 { fg } else { bg };
+ let (r, g, b) = if value == 0 { foreground } else { background };
- ctx.set_source_rgb(r, g, b);
- ctx.rectangle(x as f64, y as f64, 1.0, 1.0);
- ctx.fill()?;
+ context.set_source_rgb(r, g, b);
+ context.rectangle(x as f64, y as f64, 1.0, 1.0);
+ context.fill()?;
}
}
}
- let pattern = cairo::SurfacePattern::create(&surface);
- pattern.set_extend(cairo::Extend::Repeat);
+ let pattern = SurfacePattern::create(&surface);
+ pattern.set_extend(Extend::Repeat);
Ok(pattern)
}
@@ -66,12 +62,11 @@ mod tests {
#[test]
fn test_pattern_values() {
- // All pattern values should be 0 or 1
- for &val in &STITCH {
- assert!(val == 0 || val == 1);
+ for &pixel in &STITCH {
+ assert!(pixel == 0 || pixel == 1);
}
- for &val in &SHINGLES {
- assert!(val == 0 || val == 1);
+ for &pixel in &SHINGLES {
+ assert!(pixel == 0 || pixel == 1);
}
}
}
diff --git a/src/renderer.rs b/src/renderer.rs
index 74f4708..a673bac 100644
--- a/src/renderer.rs
+++ b/src/renderer.rs
@@ -1,65 +1,108 @@
+use std::f64::consts::PI;
+use std::fs::read_to_string;
use std::io::Cursor;
-use wmap_parser::Map;
+
+use cairo::{
+ Context, Error, FontSlant, FontWeight, Format, ImageSurface, LineCap, LineJoin, SvgSurface,
+};
+use geo::{ConcaveHull, LineString, Point};
+use rustc_hash::FxHashMap;
+use tempfile::NamedTempFile;
+use wmap_parser::{Map, Stage};
use crate::configuration::Configuration;
+use crate::error::RenderError;
+use crate::patterns::{SHADOW_GRID, SHINGLES, STITCH, WICKER, create_pattern};
+use crate::shapes::draw_vertex;
+use crate::smart_positioning::{
+ PositioningDimensions, build_spatial_grid, calculate_optimal_label_position_cached,
+ collect_all_lines, collect_note_rects,
+};
use crate::stage_type::StageType;
-use crate::utils::{percent_to_pixel, set_color};
-use crate::RenderError;
-use cairo::{Context, Error, FontSlant, FontWeight, Format, ImageSurface, LineCap, LineJoin, SvgSurface};
+use crate::utils::percent_to_pixel;
-/// Renders a map to PNG format
+/// Renders a Wardley map to PNG format
+///
+/// # Arguments
+///
+/// * `map` - The parsed Wardley map
+/// * `stage_type` - The stage type to use for axis labels
+/// * `config` - Configuration options
+///
+/// # Returns
+///
+/// A vector of bytes containing the PNG image data
pub fn render_to_png(
map: &Map,
stage_type: StageType,
- config: &Configuration,
+ configuration: &Configuration,
) -> Result<Vec<u8>, RenderError> {
- let total_width = (config.theme.sizes.map_width + config.theme.sizes.padding * 2.0) as i32;
- let total_height = (config.theme.sizes.map_height
- + config.theme.sizes.padding
- + config.theme.sizes.bottom_padding) as i32;
+ let total_width =
+ (configuration.theme.sizes.map_width + configuration.theme.sizes.padding * 2.0) as i32;
+ let total_height = (configuration.theme.sizes.map_height
+ + configuration.theme.sizes.padding
+ + configuration.theme.sizes.bottom_padding) as i32;
let surface = ImageSurface::create(Format::ARgb32, total_width, total_height)?;
- let ctx = Context::new(&surface)?;
+ let context = Context::new(&surface)?;
- render(&ctx, map, stage_type, config)?;
+ render(&context, map, stage_type, configuration)?;
let mut buffer = Cursor::new(Vec::new());
surface.write_to_png(&mut buffer)?;
Ok(buffer.into_inner())
}
-/// Renders a map to the context
+/// Renders a Wardley map to a cairo context.
+///
+/// # Arguments
+///
+/// * `map` - The parsed Wardley map
+/// * `stage_type` - The stage type to use for axis labels
+/// * `config` - Configuration options
+///
+/// # Returns
+///
+/// A string containing the SVG data
pub fn render_to_surface(
map: &Map,
stage_type: StageType,
- config: &Configuration,
+ configuration: &Configuration,
) -> Result<ImageSurface, RenderError> {
- let total_width = (config.theme.sizes.map_width + config.theme.sizes.padding * 2.0) as i32;
- let total_height = (config.theme.sizes.map_height
- + config.theme.sizes.padding
- + config.theme.sizes.bottom_padding) as i32;
+ let total_width =
+ (configuration.theme.sizes.map_width + configuration.theme.sizes.padding * 2.0) as i32;
+ let total_height = (configuration.theme.sizes.map_height
+ + configuration.theme.sizes.padding
+ + configuration.theme.sizes.bottom_padding) as i32;
let surface = ImageSurface::create(Format::ARgb32, total_width, total_height)?;
- let ctx = Context::new(&surface)?;
+ let context = Context::new(&surface)?;
- render(&ctx, map, stage_type, config)?;
+ render(&context, map, stage_type, configuration)?;
Ok(surface)
}
-/// Renders a map to SVG format
+/// Renders a Wardley map to SVG format
+///
+/// # Arguments
+///
+/// * `map` - The parsed Wardley map
+/// * `stage_type` - The stage type to use for axis labels
+/// * `config` - Configuration options
+///
+/// # Returns
+///
+/// A string containing the SVG data
pub fn render_to_svg(
map: &Map,
stage_type: StageType,
- config: &Configuration,
+ configuration: &Configuration,
) -> Result<String, RenderError> {
- use std::fs;
- use tempfile::NamedTempFile;
-
- let total_width = config.theme.sizes.map_width + config.theme.sizes.padding * 2.0;
- let total_height = config.theme.sizes.map_height
- + config.theme.sizes.padding
- + config.theme.sizes.bottom_padding;
+ let total_width = configuration.theme.sizes.map_width + configuration.theme.sizes.padding * 2.0;
+ let total_height = configuration.theme.sizes.map_height
+ + configuration.theme.sizes.padding
+ + configuration.theme.sizes.bottom_padding;
let temp_file = NamedTempFile::new()?;
let path = temp_file
@@ -68,201 +111,204 @@ pub fn render_to_svg(
.ok_or_else(|| RenderError::InvalidConfig("Failed to create temp file path".to_string()))?;
let surface = SvgSurface::new(total_width, total_height, Some(path))?;
- let ctx = Context::new(&surface)?;
+ let context = Context::new(&surface)?;
- render(&ctx, map, stage_type, config)?;
+ render(&context, map, stage_type, configuration)?;
- drop(ctx);
+ drop(context);
surface.finish();
- let svg_content = fs::read_to_string(path)?;
+ let svg_content = read_to_string(path)?;
Ok(svg_content)
}
/// Core rendering function
fn render(
- ctx: &Context,
+ context: &Context,
map: &Map,
stage_type: StageType,
- config: &Configuration,
+ configuration: &Configuration,
) -> Result<(), RenderError> {
- use std::collections::HashMap;
-
// Apply padding transform
- ctx.save()?;
- ctx.translate(config.theme.sizes.padding, config.theme.sizes.padding);
+ context.save()?;
+ context.translate(
+ configuration.theme.sizes.padding,
+ configuration.theme.sizes.padding,
+ );
- // Build component map once and reuse it
- let component_map: HashMap<String, (f64, f64)> = map.components
- .iter()
- .map(|component| {
- let x = percent_to_pixel(component.coordinates.0, config.theme.sizes.map_width);
- let y = percent_to_pixel(component.coordinates.1, config.theme.sizes.map_height);
- (component.label.to_lowercase(), (x, y))
- })
- .collect();
+ // Build component map once and reuse it (pre-allocate capacity for better performance)
+ // Use FxHashMap for faster hashing (we don't need cryptographic security)
+ let mut component_map: FxHashMap<String, (f64, f64)> =
+ FxHashMap::with_capacity_and_hasher(map.components.len(), Default::default());
+ for component in &map.components {
+ let x = percent_to_pixel(component.coordinates.0, configuration.theme.sizes.map_width);
+ let y = percent_to_pixel(
+ component.coordinates.1,
+ configuration.theme.sizes.map_height,
+ );
+ component_map.insert(component.label.to_lowercase(), (x, y));
+ }
// Render in order (back to front)
- draw_background(ctx, config)?;
+ draw_background(context, configuration)?;
- if config.options.show_background {
- draw_stage_patterns(ctx, stage_type, map, config)?;
+ if configuration.options.show_background {
+ draw_stage_patterns(context, stage_type, map, configuration)?;
}
- draw_axes(ctx, stage_type, map, config)?;
- draw_stage_dividers(ctx, map, config)?;
- draw_dependencies(ctx, map, config, &component_map)?;
- draw_groups(ctx, map, config, &component_map)?;
- draw_vertices(ctx, map, config)?;
- draw_vertex_labels(ctx, map, config, &component_map)?;
- draw_inertias(ctx, map, config, &component_map)?;
- draw_evolutions(ctx, map, config, &component_map)?;
- draw_notes(ctx, map, config)?;
+ draw_axes(context, stage_type, map, configuration)?;
+ draw_stage_dividers(context, map, configuration)?;
+ draw_dependencies(context, map, configuration, &component_map)?;
+ draw_groups(context, map, configuration, &component_map)?;
+ draw_vertices(context, map, configuration)?;
+ draw_vertex_labels(context, map, configuration, &component_map)?;
+ draw_inertias(context, map, configuration, &component_map)?;
+ draw_evolutions(context, map, configuration, &component_map)?;
+ draw_notes(context, map, configuration)?;
- ctx.restore()?;
+ context.restore()?;
Ok(())
}
-fn draw_background(ctx: &Context, config: &Configuration) -> Result<(), RenderError> {
- ctx.save()?;
- set_color(ctx, &config.theme.colors.background);
- ctx.paint()?;
- ctx.restore()?;
+fn draw_background(context: &Context, configuration: &Configuration) -> Result<(), RenderError> {
+ context.save()?;
+ let (r, g, b) = configuration.theme.colors.background;
+ context.set_source_rgb(r, g, b);
+ context.paint()?;
+ context.restore()?;
Ok(())
}
fn draw_stage_patterns(
- ctx: &Context,
+ context: &Context,
_stage_type: StageType,
map: &Map,
- config: &Configuration,
+ configuration: &Configuration,
) -> Result<(), RenderError> {
- use crate::patterns::{create_pattern, SHADOW_GRID, SHINGLES, STITCH, WICKER};
+ let stages = get_stage_positions(map, configuration);
- let stages = get_stage_positions(map, config);
- let patterns = [
- create_pattern(
- &STITCH,
- &config.theme.colors.stage_foreground,
- &config.theme.colors.stage_background,
- )?,
- create_pattern(
- &SHINGLES,
- &config.theme.colors.stage_foreground,
- &config.theme.colors.stage_background,
- )?,
- create_pattern(
- &SHADOW_GRID,
- &config.theme.colors.stage_foreground,
- &config.theme.colors.stage_background,
- )?,
- create_pattern(
- &WICKER,
- &config.theme.colors.stage_foreground,
- &config.theme.colors.stage_background,
- )?,
- ];
+ // Create patterns once (they're small and fast to create)
+ let pattern0 = create_pattern(
+ &STITCH,
+ configuration.theme.colors.stage_foreground,
+ configuration.theme.colors.stage_background,
+ )?;
+ let pattern1 = create_pattern(
+ &SHINGLES,
+ configuration.theme.colors.stage_foreground,
+ configuration.theme.colors.stage_background,
+ )?;
+ let pattern2 = create_pattern(
+ &SHADOW_GRID,
+ configuration.theme.colors.stage_foreground,
+ configuration.theme.colors.stage_background,
+ )?;
+ let pattern3 = create_pattern(
+ &WICKER,
+ configuration.theme.colors.stage_foreground,
+ configuration.theme.colors.stage_background,
+ )?;
- ctx.save()?;
+ context.save()?;
// Draw pattern for stage 0 to stage 1
- ctx.rectangle(0.0, 0.0, stages[0], config.theme.sizes.map_height);
- ctx.set_source(&patterns[0])?;
- ctx.fill()?;
+ context.rectangle(0.0, 0.0, stages[0], configuration.theme.sizes.map_height);
+ context.set_source(&pattern0)?;
+ context.fill()?;
// Draw pattern for stage 1 to stage 2
- ctx.rectangle(
+ context.rectangle(
stages[0],
0.0,
stages[1] - stages[0],
- config.theme.sizes.map_height,
+ configuration.theme.sizes.map_height,
);
- ctx.set_source(&patterns[1])?;
- ctx.fill()?;
+ context.set_source(&pattern1)?;
+ context.fill()?;
// Draw pattern for stage 2 to stage 3
- ctx.rectangle(
+ context.rectangle(
stages[1],
0.0,
stages[2] - stages[1],
- config.theme.sizes.map_height,
+ configuration.theme.sizes.map_height,
);
- ctx.set_source(&patterns[2])?;
- ctx.fill()?;
+ context.set_source(&pattern2)?;
+ context.fill()?;
// Draw pattern for stage 3 to end
- ctx.rectangle(
+ context.rectangle(
stages[2],
0.0,
- config.theme.sizes.map_width - stages[2],
- config.theme.sizes.map_height,
+ configuration.theme.sizes.map_width - stages[2],
+ configuration.theme.sizes.map_height,
);
- ctx.set_source(&patterns[3])?;
- ctx.fill()?;
+ context.set_source(&pattern3)?;
+ context.fill()?;
- ctx.restore()?;
+ context.restore()?;
Ok(())
}
fn draw_axes(
- ctx: &Context,
+ context: &Context,
stage_type: StageType,
map: &Map,
- config: &Configuration,
+ configuration: &Configuration,
) -> Result<(), RenderError> {
- use std::f64::consts::PI;
+ let sizes = &configuration.theme.sizes;
+ let fonts = &configuration.theme.fonts;
- let sizes = &config.theme.sizes;
- let fonts = &config.theme.fonts;
-
- ctx.save()?;
- set_color(ctx, &config.theme.colors.axis);
- ctx.set_line_width(sizes.line_width * 2.0);
+ context.save()?;
+ let (r, g, b) = configuration.theme.colors.axis;
+ context.set_source_rgb(r, g, b);
+ context.set_line_width(sizes.line_width * 2.0);
// Y-axis and X-axis
- ctx.move_to(0.0, 0.0);
- ctx.line_to(0.0, sizes.map_height);
- ctx.line_to(sizes.map_width, sizes.map_height);
- ctx.stroke()?;
+ context.move_to(0.0, 0.0);
+ context.line_to(0.0, sizes.map_height);
+ context.line_to(sizes.map_width, sizes.map_height);
+ context.stroke()?;
// Text setup
- set_color(ctx, &config.theme.colors.label);
- ctx.select_font_face(
- "sans-serif",
+ let (r, g, b) = configuration.theme.colors.label;
+ context.set_source_rgb(r, g, b);
+ context.select_font_face(
+ &configuration.theme.fonts.face,
FontSlant::Normal,
FontWeight::Normal,
);
- ctx.set_font_size(fonts.axis_label);
- ctx.set_line_width(1.0);
+ context.set_font_size(fonts.axis_label);
+ context.set_line_width(1.0);
// Y-axis labels (rotated -90 degrees)
- ctx.save()?;
- ctx.translate(-20.0, 45.0);
- ctx.rotate(-PI / 2.0);
- ctx.move_to(0.0, 0.0);
- ctx.show_text("Visible")?;
- ctx.restore()?;
+ context.save()?;
+ context.translate(-20.0, 45.0);
+ context.rotate(-PI / 2.0);
+ context.move_to(0.0, 0.0);
+ context.show_text("Visible")?;
+ context.restore()?;
- ctx.save()?;
- ctx.translate(-20.0, sizes.map_height);
- ctx.rotate(-PI / 2.0);
- ctx.move_to(0.0, 0.0);
- ctx.show_text("Invisible")?;
- ctx.restore()?;
+ context.save()?;
+ context.translate(-20.0, sizes.map_height);
+ context.rotate(-PI / 2.0);
+ context.move_to(0.0, 0.0);
+ context.show_text("Invisible")?;
+ context.restore()?;
// X-axis labels (above the map, at negative Y, offset by half vertex height)
let stage_height = sizes.stage_height;
let text_offset_y = -stage_height / 4.0 + sizes.vertex_size / 2.0;
- ctx.move_to(0.0, text_offset_y);
- ctx.show_text("Uncharted")?;
+ context.move_to(0.0, text_offset_y);
+ context.show_text("Uncharted")?;
- let text_ext = ctx.text_extents("Industrialised")?;
- ctx.move_to(sizes.map_width - text_ext.width(), text_offset_y);
- ctx.show_text("Industrialised")?;
+ let text_ext = context.text_extents("Industrialised")?;
+ context.move_to(sizes.map_width - text_ext.width(), text_offset_y);
+ context.show_text("Industrialised")?;
// Stage labels (below the map, offset by half vertex height, with text wrapping)
let stage_labels = stage_type.stages();
- let stages = get_stage_positions(map, config);
+ let stages = get_stage_positions(map, configuration);
let padding = 5.0; // kAxisLabelPadding
let stage_label_y = sizes.map_height + padding + sizes.vertex_size / 2.0;
let line_height = fonts.axis_label * 1.2; // 120% line height
@@ -285,55 +331,55 @@ fn draw_axes(
_ => continue,
};
- let wrapped_lines = wrap_text(ctx, label, stage_widths[i])?;
+ let wrapped_lines = wrap_text(context, label, stage_widths[i])?;
for (line_index, line) in wrapped_lines.iter().enumerate() {
let y = stage_label_y + line_index as f64 * line_height;
- ctx.move_to(x, y);
- ctx.show_text(line)?;
+ context.move_to(x, y);
+ context.show_text(line)?;
}
}
- ctx.restore()?;
+ context.restore()?;
Ok(())
}
fn draw_stage_dividers(
- ctx: &Context,
+ context: &Context,
map: &Map,
- config: &Configuration,
+ configuration: &Configuration,
) -> Result<(), RenderError> {
- let stages = get_stage_positions(map, config);
+ let stages = get_stage_positions(map, configuration);
- ctx.save()?;
- set_color(ctx, &config.theme.colors.axis);
- ctx.set_line_width(config.theme.sizes.line_width * 2.0);
- ctx.set_dash(&[10.0, 18.0], 0.0);
+ context.save()?;
+ let (r, g, b) = configuration.theme.colors.axis;
+ context.set_source_rgb(r, g, b);
+ context.set_line_width(configuration.theme.sizes.line_width * 2.0);
+ context.set_dash(&[10.0, 18.0], 0.0);
for &stage_x in &stages {
- ctx.move_to(stage_x, 0.0);
- ctx.line_to(stage_x, config.theme.sizes.map_height);
+ context.move_to(stage_x, 0.0);
+ context.line_to(stage_x, configuration.theme.sizes.map_height);
}
- ctx.stroke()?;
- ctx.restore()?;
+ context.stroke()?;
+ context.restore()?;
Ok(())
}
fn draw_dependencies(
- ctx: &Context,
+ context: &Context,
map: &Map,
- config: &Configuration,
- component_map: &std::collections::HashMap<String, (f64, f64)>,
+ configuration: &Configuration,
+ component_map: &rustc_hash::FxHashMap<String, (f64, f64)>,
) -> Result<(), RenderError> {
- use std::f64::consts::PI;
-
- ctx.save()?;
- set_color(ctx, &config.theme.colors.vertex);
- ctx.set_line_width(config.theme.sizes.line_width);
+ context.save()?;
+ let (r, g, b) = configuration.theme.colors.vertex;
+ context.set_source_rgb(r, g, b);
+ context.set_line_width(configuration.theme.sizes.line_width);
- let half_width = config.theme.sizes.vertex_size / 2.0;
- let half_height = config.theme.sizes.vertex_size / 2.0;
+ let half_width = configuration.theme.sizes.vertex_size / 2.0;
+ let half_height = configuration.theme.sizes.vertex_size / 2.0;
let arrow_angle = PI / 4.0; // 45 degrees
for dependency in &map.dependencies {
@@ -362,21 +408,21 @@ fn draw_dependencies(
let offset_dest_y = dest_y - half_height * sine;
// Draw line
- ctx.move_to(offset_origin_x, offset_origin_y);
- ctx.line_to(offset_dest_x, offset_dest_y);
+ context.move_to(offset_origin_x, offset_origin_y);
+ context.line_to(offset_dest_x, offset_dest_y);
// Draw arrowhead if directed
if dependency.is_directed {
let upper_angle = angle - arrow_angle;
let lower_angle = angle + arrow_angle;
- let arrowhead_size = config.theme.sizes.arrowhead_size;
+ let arrowhead_size = configuration.theme.sizes.arrowhead_size;
- ctx.move_to(
+ context.move_to(
offset_dest_x - arrowhead_size * upper_angle.cos(),
offset_dest_y - arrowhead_size * upper_angle.sin(),
);
- ctx.line_to(offset_dest_x, offset_dest_y);
- ctx.line_to(
+ context.line_to(offset_dest_x, offset_dest_y);
+ context.line_to(
offset_dest_x - arrowhead_size * lower_angle.cos(),
offset_dest_y - arrowhead_size * lower_angle.sin(),
);
@@ -384,35 +430,34 @@ fn draw_dependencies(
}
// Batch stroke all paths at once
- ctx.stroke()?;
+ context.stroke()?;
- ctx.restore()?;
+ context.restore()?;
Ok(())
}
fn draw_groups(
- ctx: &Context,
+ context: &Context,
map: &Map,
- config: &Configuration,
- component_map: &std::collections::HashMap<String, (f64, f64)>,
+ configuration: &Configuration,
+ component_map: &rustc_hash::FxHashMap<String, (f64, f64)>,
) -> Result<(), RenderError> {
- use geo::{ConcaveHull, LineString, Point};
- use std::f64::consts::PI;
-
if map.groups.is_empty() {
return Ok(());
}
// Create ONE offscreen surface for ALL groups for opacity handling
- let width = (config.theme.sizes.map_width + config.theme.sizes.vertex_size) as i32;
- let height = (config.theme.sizes.map_height + config.theme.sizes.vertex_size) as i32;
+ let width =
+ (configuration.theme.sizes.map_width + configuration.theme.sizes.vertex_size) as i32;
+ let height =
+ (configuration.theme.sizes.map_height + configuration.theme.sizes.vertex_size) as i32;
let offscreen = ImageSurface::create(Format::ARgb32, width, height)?;
- let offscreen_ctx = Context::new(&offscreen)?;
+ let offscreen_context = Context::new(&offscreen)?;
// Translate to account for vertex width/height
- offscreen_ctx.translate(
- config.theme.sizes.vertex_size / 2.0,
- config.theme.sizes.vertex_size / 2.0,
+ offscreen_context.translate(
+ configuration.theme.sizes.vertex_size / 2.0,
+ configuration.theme.sizes.vertex_size / 2.0,
);
for (group_index, group) in map.groups.iter().enumerate() {
@@ -426,7 +471,8 @@ fn draw_groups(
continue;
}
- let color = &config.theme.colors.groups[group_index % config.theme.colors.groups.len()];
+ let color = &configuration.theme.colors.groups
+ [group_index % configuration.theme.colors.groups.len()];
let is_line = component_coords.len() == 2;
let coords = if component_coords.len() == 1 {
@@ -451,7 +497,7 @@ fn draw_groups(
.collect();
let line_string: LineString = points.into();
- let hull = line_string.concave_hull(2.0);
+ let hull = line_string.concave_hull();
hull.exterior().coords().map(|c| (c.x, c.y)).collect()
};
@@ -460,163 +506,205 @@ fn draw_groups(
}
// Draw path on offscreen context
- set_color(&offscreen_ctx, color);
- offscreen_ctx.move_to(coords[0].0, coords[0].1);
+ let (r, g, b) = *color;
+ offscreen_context.set_source_rgb(r, g, b);
+ offscreen_context.move_to(coords[0].0, coords[0].1);
for &(x, y) in &coords[1..] {
- offscreen_ctx.line_to(x, y);
+ offscreen_context.line_to(x, y);
}
// For polygons (not lines), close path and fill
if !is_line {
- offscreen_ctx.close_path();
- offscreen_ctx.fill_preserve()?;
+ offscreen_context.close_path();
+ offscreen_context.fill_preserve()?;
}
// Draw stroke with round caps and joins for smoother appearance
- offscreen_ctx.set_line_width(config.theme.sizes.vertex_size * 1.75);
- offscreen_ctx.set_line_cap(LineCap::Round);
- offscreen_ctx.set_line_join(LineJoin::Round);
- offscreen_ctx.stroke()?;
+ offscreen_context.set_line_width(configuration.theme.sizes.vertex_size * 1.75);
+ offscreen_context.set_line_cap(LineCap::Round);
+ offscreen_context.set_line_join(LineJoin::Round);
+ offscreen_context.stroke()?;
}
// Composite entire offscreen surface onto main context with opacity (once for all groups)
- ctx.save()?;
- ctx.set_source_surface(&offscreen, 0.0, 0.0)?;
- ctx.paint_with_alpha(config.theme.opacity.groups)?;
- ctx.restore()?;
+ context.save()?;
+ context.set_source_surface(&offscreen, 0.0, 0.0)?;
+ context.paint_with_alpha(configuration.theme.opacity.groups)?;
+ context.restore()?;
Ok(())
}
fn draw_vertices(
- ctx: &Context,
+ context: &Context,
map: &Map,
- config: &Configuration,
+ configuration: &Configuration,
) -> Result<(), RenderError> {
- use crate::shapes::draw_vertex;
-
- ctx.save()?;
- set_color(ctx, &config.theme.colors.vertex);
+ context.save()?;
+ let (r, g, b) = configuration.theme.colors.vertex;
+ context.set_source_rgb(r, g, b);
for component in &map.components {
- let x = percent_to_pixel(component.coordinates.0, config.theme.sizes.map_width);
- let y = percent_to_pixel(component.coordinates.1, config.theme.sizes.map_height);
+ let x = percent_to_pixel(component.coordinates.0, configuration.theme.sizes.map_width);
+ let y = percent_to_pixel(
+ component.coordinates.1,
+ configuration.theme.sizes.map_height,
+ );
draw_vertex(
- ctx,
+ context,
&component.shape,
x,
y,
- config.theme.sizes.vertex_size,
- config.theme.sizes.vertex_size,
+ configuration.theme.sizes.vertex_size,
+ configuration.theme.sizes.vertex_size,
)?;
}
- ctx.restore()?;
+ context.restore()?;
Ok(())
}
fn draw_vertex_labels(
- ctx: &Context,
+ context: &Context,
map: &Map,
- config: &Configuration,
- component_map: &std::collections::HashMap<String, (f64, f64)>,
+ configuration: &Configuration,
+ component_map: &rustc_hash::FxHashMap<String, (f64, f64)>,
) -> Result<(), RenderError> {
- ctx.save()?;
- set_color(ctx, &config.theme.colors.label);
- ctx.select_font_face(
- "sans-serif",
+ context.save()?;
+ let (r, g, b) = configuration.theme.colors.label;
+ context.set_source_rgb(r, g, b);
+ context.select_font_face(
+ &configuration.theme.fonts.face,
FontSlant::Normal,
FontWeight::Normal,
);
- ctx.set_font_size(config.theme.fonts.vertex_label);
+ context.set_font_size(configuration.theme.fonts.vertex_label);
- if config.options.smart_label_positioning {
- // Pre-compute collision data once for all labels
- use crate::smart_positioning::{collect_all_lines, collect_note_rects, calculate_optimal_label_position_cached};
+ if configuration.options.smart_label_positioning {
+ // For maps with very few dependencies, smart positioning overhead isn't worth it
+ let has_significant_complexity = map.dependencies.len() + map.evolutions.len() > 10;
- let all_lines = collect_all_lines(
- map,
- config.theme.sizes.map_width,
- config.theme.sizes.map_height,
- config.theme.sizes.vertex_size,
- component_map,
- );
+ if has_significant_complexity {
+ // Pre-compute collision data once for all labels
- let note_rects = collect_note_rects(
- map,
- config.theme.sizes.map_width,
- config.theme.sizes.map_height,
- ctx,
- config.theme.fonts.vertex_label,
- )?;
+ let all_lines = collect_all_lines(
+ map,
+ configuration.theme.sizes.map_width,
+ configuration.theme.sizes.map_height,
+ configuration.theme.sizes.vertex_size,
+ component_map,
+ );
- for component in &map.components {
- let position = calculate_optimal_label_position_cached(
- component,
- config.theme.sizes.map_width,
- config.theme.sizes.map_height,
- config.theme.sizes.vertex_size,
- ctx,
- config.theme.fonts.vertex_label,
+ let spatial_grid = build_spatial_grid(
&all_lines,
- &note_rects,
+ configuration.theme.sizes.map_width,
+ configuration.theme.sizes.map_height,
+ );
+
+ let note_rects = collect_note_rects(
+ map,
+ configuration.theme.sizes.map_width,
+ configuration.theme.sizes.map_height,
+ context,
+ configuration.theme.fonts.vertex_label,
)?;
- let label_x = position.x;
- let label_y = position.y + 7.0; // Adjust Y position for text baseline
+ for component in &map.components {
+ let position = calculate_optimal_label_position_cached(
+ component,
+ &PositioningDimensions {
+ map_size: (
+ configuration.theme.sizes.map_width,
+ configuration.theme.sizes.map_height,
+ ),
+ vertex_size: configuration.theme.sizes.vertex_size,
+ font_size: configuration.theme.fonts.vertex_label,
+ },
+ context,
+ &all_lines,
+ &spatial_grid,
+ &note_rects,
+ )?;
+
+ let label_x = position.x;
+ let label_y = position.y + 7.0; // Adjust Y position for text baseline
+
+ context.move_to(label_x, label_y);
+ context.show_text(&component.label)?;
+ }
+ } else {
+ // Use simple positioning for simple maps
+ for component in &map.components {
+ let x =
+ percent_to_pixel(component.coordinates.0, configuration.theme.sizes.map_width);
+ let y = percent_to_pixel(
+ component.coordinates.1,
+ configuration.theme.sizes.map_height,
+ );
+ let label_x = x + configuration.theme.sizes.vertex_size + 5.0;
+ let label_y = y
+ + configuration.theme.sizes.vertex_size / 2.0
+ + configuration.theme.fonts.vertex_label / 3.0;
- ctx.move_to(label_x, label_y);
- ctx.show_text(&component.label)?;
+ context.move_to(label_x, label_y);
+ context.show_text(&component.label)?;
+ }
}
} else {
// Simple label positioning (to the right of vertex)
for component in &map.components {
- let x = percent_to_pixel(component.coordinates.0, config.theme.sizes.map_width);
- let y = percent_to_pixel(component.coordinates.1, config.theme.sizes.map_height);
- let label_x = x + config.theme.sizes.vertex_size + 5.0;
- let label_y = y + config.theme.sizes.vertex_size / 2.0 + config.theme.fonts.vertex_label / 3.0;
+ let x = percent_to_pixel(component.coordinates.0, configuration.theme.sizes.map_width);
+ let y = percent_to_pixel(
+ component.coordinates.1,
+ configuration.theme.sizes.map_height,
+ );
+ let label_x = x + configuration.theme.sizes.vertex_size + 5.0;
+ let label_y = y
+ + configuration.theme.sizes.vertex_size / 2.0
+ + configuration.theme.fonts.vertex_label / 3.0;
- ctx.move_to(label_x, label_y);
- ctx.show_text(&component.label)?;
+ context.move_to(label_x, label_y);
+ context.show_text(&component.label)?;
}
}
- ctx.restore()?;
+ context.restore()?;
Ok(())
}
fn draw_inertias(
- ctx: &Context,
+ context: &Context,
map: &Map,
- config: &Configuration,
- component_map: &std::collections::HashMap<String, (f64, f64)>,
+ configuration: &Configuration,
+ component_map: &rustc_hash::FxHashMap<String, (f64, f64)>,
) -> Result<(), RenderError> {
- ctx.save()?;
- set_color(ctx, &config.theme.colors.inertia);
+ context.save()?;
+ let (r, g, b) = configuration.theme.colors.inertia;
+ context.set_source_rgb(r, g, b);
let corner_radius = 2.0;
- let rect_width = config.theme.sizes.vertex_size / 2.0;
- let rect_height = config.theme.sizes.vertex_size * 2.0;
+ let rect_width = configuration.theme.sizes.vertex_size / 2.0;
+ let rect_height = configuration.theme.sizes.vertex_size * 2.0;
for inertia in &map.inertias {
if let Some(&(origin_x, origin_y)) = component_map.get(&inertia.component.to_lowercase()) {
// Position relative to vertex top-left corner
- let x = origin_x + 3.0 * config.theme.sizes.vertex_size;
- let y = origin_y - (config.theme.sizes.vertex_size * 2.0) / 3.0;
+ let x = origin_x + 3.0 * configuration.theme.sizes.vertex_size;
+ let y = origin_y - (configuration.theme.sizes.vertex_size * 2.0) / 3.0;
// Draw rounded rectangle
- draw_rounded_rect(ctx, x, y, rect_width, rect_height, corner_radius)?;
- ctx.fill()?;
+ draw_rounded_rect(context, x, y, rect_width, rect_height, corner_radius)?;
+ context.fill()?;
}
}
- ctx.restore()?;
+ context.restore()?;
Ok(())
}
fn draw_rounded_rect(
- ctx: &Context,
+ context: &Context,
x: f64,
y: f64,
width: f64,
@@ -624,45 +712,41 @@ fn draw_rounded_rect(
radius: f64,
) -> Result<(), Error> {
let r = radius;
- ctx.new_path();
- ctx.arc(
+ context.new_path();
+ context.arc(
x + r,
y + r,
r,
std::f64::consts::PI,
3.0 * std::f64::consts::PI / 2.0,
);
- ctx.arc(
+ context.arc(
x + width - r,
y + r,
r,
3.0 * std::f64::consts::PI / 2.0,
0.0,
);
- ctx.arc(
+ context.arc(
x + width - r,
y + height - r,
r,
0.0,
std::f64::consts::PI / 2.0,
);
- ctx.arc(
+ context.arc(
x + r,
y + height - r,
r,
std::f64::consts::PI / 2.0,
std::f64::consts::PI,
);
- ctx.close_path();
+ context.close_path();
Ok(())
}
/// Wraps text to fit within a given width, breaking at word boundaries
-fn wrap_text(
- ctx: &Context,
- text: &str,
- max_width: f64,
-) -> Result<Vec<String>, Error> {
+fn wrap_text(context: &Context, text: &str, max_width: f64) -> Result<Vec<String>, Error> {
let words: Vec<&str> = text.split_whitespace().collect();
let mut lines = Vec::new();
let mut current_line = String::new();
@@ -674,7 +758,7 @@ fn wrap_text(
format!("{} {}", current_line, word)
};
- let extents = ctx.text_extents(&test_line)?;
+ let extents = context.text_extents(&test_line)?;
if extents.width() <= max_width {
current_line = test_line;
@@ -699,26 +783,26 @@ fn wrap_text(
}
fn draw_evolutions(
- ctx: &Context,
+ context: &Context,
map: &Map,
- config: &Configuration,
- component_map: &std::collections::HashMap<String, (f64, f64)>,
+ configuration: &Configuration,
+ component_map: &rustc_hash::FxHashMap<String, (f64, f64)>,
) -> Result<(), RenderError> {
- use std::f64::consts::PI;
-
- ctx.save()?;
- set_color(ctx, &config.theme.colors.evolution);
- ctx.set_line_width(config.theme.sizes.line_width * 2.0);
- ctx.set_dash(&[10.0], 0.0);
+ context.save()?;
+ let (r, g, b) = configuration.theme.colors.evolution;
+ context.set_source_rgb(r, g, b);
+ context.set_line_width(configuration.theme.sizes.line_width * 2.0);
+ context.set_dash(&[10.0], 0.0);
for evolution in &map.evolutions {
if let Some(&(origin_x, origin_y)) = component_map.get(&evolution.component.to_lowercase())
{
// Calculate destination with clamping
- let evolution_offset = percent_to_pixel(evolution.value, config.theme.sizes.map_width);
+ let evolution_offset =
+ percent_to_pixel(evolution.value, configuration.theme.sizes.map_width);
let dest_x = (origin_x + evolution_offset)
.max(0.0)
- .min(config.theme.sizes.map_width);
+ .min(configuration.theme.sizes.map_width);
let dest_y = origin_y;
// Determine direction
@@ -726,30 +810,30 @@ fn draw_evolutions(
// Calculate offsets from top-left corner to center the line on the vertex
let offset_origin_x = origin_x
- + multiplier * (config.theme.sizes.vertex_size / 2.0)
- + config.theme.sizes.vertex_size / 2.0;
- let offset_origin_y = origin_y + config.theme.sizes.vertex_size / 2.0;
- let offset_dest_x = dest_x - multiplier * (config.theme.sizes.vertex_size / 2.0)
- + config.theme.sizes.vertex_size / 2.0;
- let offset_dest_y = dest_y + config.theme.sizes.vertex_size / 2.0;
+ + multiplier * (configuration.theme.sizes.vertex_size / 2.0)
+ + configuration.theme.sizes.vertex_size / 2.0;
+ let offset_origin_y = origin_y + configuration.theme.sizes.vertex_size / 2.0;
+ let offset_dest_x = dest_x - multiplier * (configuration.theme.sizes.vertex_size / 2.0)
+ + configuration.theme.sizes.vertex_size / 2.0;
+ let offset_dest_y = dest_y + configuration.theme.sizes.vertex_size / 2.0;
// Draw dashed line
- ctx.move_to(offset_origin_x, offset_origin_y);
- ctx.line_to(offset_dest_x, offset_dest_y);
+ context.move_to(offset_origin_x, offset_origin_y);
+ context.line_to(offset_dest_x, offset_dest_y);
// Draw arrowhead at destination only
let upper_angle = -PI / 4.0;
let lower_angle = PI / 4.0;
- let arrowhead_size = config.theme.sizes.arrowhead_size;
+ let arrowhead_size = configuration.theme.sizes.arrowhead_size;
- ctx.move_to(offset_dest_x, offset_dest_y);
- ctx.line_to(
+ context.move_to(offset_dest_x, offset_dest_y);
+ context.line_to(
offset_dest_x - multiplier * arrowhead_size * upper_angle.cos(),
offset_dest_y - multiplier * arrowhead_size * upper_angle.sin(),
);
- ctx.move_to(offset_dest_x, offset_dest_y);
- ctx.line_to(
+ context.move_to(offset_dest_x, offset_dest_y);
+ context.line_to(
offset_dest_x - multiplier * arrowhead_size * lower_angle.cos(),
offset_dest_y - multiplier * arrowhead_size * lower_angle.sin(),
);
@@ -757,27 +841,31 @@ fn draw_evolutions(
}
// Batch stroke all paths at once
- ctx.stroke()?;
+ context.stroke()?;
- ctx.restore()?;
+ context.restore()?;
Ok(())
}
-fn draw_notes(ctx: &Context, map: &Map, config: &Configuration) -> Result<(), RenderError> {
+fn draw_notes(
+ context: &Context,
+ map: &Map,
+ configuration: &Configuration,
+) -> Result<(), RenderError> {
let padding = 5.0;
let max_width = 400.0;
- ctx.save()?;
- ctx.select_font_face(
- "sans-serif",
+ context.save()?;
+ context.select_font_face(
+ &configuration.theme.fonts.face,
FontSlant::Normal,
FontWeight::Normal,
);
- ctx.set_font_size(config.theme.fonts.note);
+ context.set_font_size(configuration.theme.fonts.note);
for note in &map.notes {
- let x = percent_to_pixel(note.coordinates.0, config.theme.sizes.map_width);
- let y = percent_to_pixel(note.coordinates.1, config.theme.sizes.map_height);
+ let x = percent_to_pixel(note.coordinates.0, configuration.theme.sizes.map_width);
+ let y = percent_to_pixel(note.coordinates.1, configuration.theme.sizes.map_height);
// Split text by newlines (handle both \\n escape and actual newlines)
let text = note.text.replace("\\n", "\n");
@@ -786,45 +874,48 @@ fn draw_notes(ctx: &Context, map: &Map, config: &Configuration) -> Result<(), Re
// Calculate box dimensions
let mut widest_line = 0.0;
for line in &lines {
- let extents = ctx.text_extents(line)?;
+ let extents = context.text_extents(line)?;
if extents.width() > widest_line {
widest_line = extents.width();
}
}
let box_width = f64::min(max_width, widest_line + padding * 2.0);
- let box_height = lines.len() as f64 * config.theme.line_heights.note + padding * 2.0;
+ let box_height = lines.len() as f64 * configuration.theme.line_heights.note + padding * 2.0;
// Draw background
- set_color(ctx, &config.theme.colors.background);
- ctx.rectangle(x, y, box_width, box_height);
- ctx.fill()?;
+ let (r, g, b) = configuration.theme.colors.background;
+ context.set_source_rgb(r, g, b);
+ context.rectangle(x, y, box_width, box_height);
+ context.fill()?;
// Draw border
- set_color(ctx, &config.theme.colors.vertex);
- ctx.set_line_width(config.theme.sizes.line_width);
- ctx.rectangle(x, y, box_width, box_height);
- ctx.stroke()?;
+ let (r, g, b) = configuration.theme.colors.vertex;
+ context.set_source_rgb(r, g, b);
+ context.set_line_width(configuration.theme.sizes.line_width);
+ context.rectangle(x, y, box_width, box_height);
+ context.stroke()?;
// Draw text
- set_color(ctx, &config.theme.colors.label);
+ let (r, g, b) = configuration.theme.colors.label;
+ context.set_source_rgb(r, g, b);
for (i, line) in lines.iter().enumerate() {
- ctx.move_to(
+ context.move_to(
x + padding,
- y + padding + i as f64 * config.theme.line_heights.note + config.theme.fonts.note,
+ y + padding
+ + i as f64 * configuration.theme.line_heights.note
+ + configuration.theme.fonts.note,
);
- ctx.show_text(line)?;
+ context.show_text(line)?;
}
}
- ctx.restore()?;
+ context.restore()?;
Ok(())
}
/// Gets the pixel positions of evolution stages
-fn get_stage_positions(map: &Map, config: &Configuration) -> [f64; 3] {
- use wmap_parser::Stage;
-
+fn get_stage_positions(map: &Map, configuration: &Configuration) -> [f64; 3] {
let default_stages = [25.0, 50.0, 75.0];
let mut stages = default_stages;
@@ -840,8 +931,8 @@ fn get_stage_positions(map: &Map, config: &Configuration) -> [f64; 3] {
}
[
- percent_to_pixel(stages[0], config.theme.sizes.map_width),
- percent_to_pixel(stages[1], config.theme.sizes.map_width),
- percent_to_pixel(stages[2], config.theme.sizes.map_width),
+ percent_to_pixel(stages[0], configuration.theme.sizes.map_width),
+ percent_to_pixel(stages[1], configuration.theme.sizes.map_width),
+ percent_to_pixel(stages[2], configuration.theme.sizes.map_width),
]
}
diff --git a/src/shapes.rs b/src/shapes.rs
index 2b8db2c..bf408be 100644
--- a/src/shapes.rs
+++ b/src/shapes.rs
@@ -1,95 +1,74 @@
+use cairo::{Context, Error};
+use std::f64::consts::PI;
use wmap_parser::Shape;
/// Draws a vertex shape at the given position
pub fn draw_vertex(
- ctx: &cairo::Context,
+ context: &Context,
shape: &Shape,
x: f64,
y: f64,
width: f64,
height: f64,
-) -> Result<(), cairo::Error> {
+) -> Result<(), Error> {
match shape {
- Shape::Circle => draw_circle(ctx, x, y, width, height),
- Shape::Square => draw_square(ctx, x, y, width, height),
- Shape::Triangle => draw_triangle(ctx, x, y, width, height),
- Shape::X => draw_x(ctx, x, y, width, height),
+ Shape::Circle => draw_circle(context, x, y, width, height),
+ Shape::Square => draw_square(context, x, y, width, height),
+ Shape::Triangle => draw_triangle(context, x, y, width, height),
+ Shape::X => draw_x(context, x, y, width, height),
}
}
-fn draw_circle(
- ctx: &cairo::Context,
- x: f64,
- y: f64,
- width: f64,
- height: f64,
-) -> Result<(), cairo::Error> {
- ctx.save()?;
- ctx.translate(x + width / 2.0, y + height / 2.0);
- ctx.scale(width / 2.0, height / 2.0);
- ctx.arc(0.0, 0.0, 1.0, 0.0, 2.0 * std::f64::consts::PI);
- ctx.restore()?;
- ctx.fill()?;
+fn draw_circle(context: &Context, x: f64, y: f64, width: f64, height: f64) -> Result<(), Error> {
+ context.save()?;
+ context.translate(x + width / 2.0, y + height / 2.0);
+ context.scale(width / 2.0, height / 2.0);
+ context.arc(0.0, 0.0, 1.0, 0.0, 2.0 * PI);
+ context.restore()?;
+ context.fill()?;
Ok(())
}
-fn draw_square(
- ctx: &cairo::Context,
- x: f64,
- y: f64,
- width: f64,
- height: f64,
-) -> Result<(), cairo::Error> {
- ctx.rectangle(x, y, width, height);
- ctx.fill()?;
+fn draw_square(context: &Context, x: f64, y: f64, width: f64, height: f64) -> Result<(), Error> {
+ context.rectangle(x, y, width, height);
+ context.fill()?;
Ok(())
}
-fn draw_triangle(
- ctx: &cairo::Context,
- x: f64,
- y: f64,
- width: f64,
- height: f64,
-) -> Result<(), cairo::Error> {
- ctx.move_to(x + width / 2.0, y);
- ctx.line_to(x + width, y + height);
- ctx.line_to(x, y + height);
- ctx.close_path();
- ctx.fill()?;
+fn draw_triangle(context: &Context, x: f64, y: f64, width: f64, height: f64) -> Result<(), Error> {
+ context.move_to(x + width / 2.0, y);
+ context.line_to(x + width, y + height);
+ context.line_to(x, y + height);
+ context.close_path();
+ context.fill()?;
Ok(())
}
-fn draw_x(
- ctx: &cairo::Context,
- x: f64,
- y: f64,
- width: f64,
- height: f64,
-) -> Result<(), cairo::Error> {
+fn draw_x(context: &Context, x: f64, y: f64, width: f64, height: f64) -> Result<(), Error> {
let line_width = 2.0;
- ctx.set_line_width(line_width);
- ctx.move_to(x, y);
- ctx.line_to(x + width, y + height);
- ctx.move_to(x + width, y);
- ctx.line_to(x, y + height);
- ctx.stroke()?;
+ context.set_line_width(line_width);
+ context.move_to(x, y);
+ context.line_to(x + width, y + height);
+ context.move_to(x + width, y);
+ context.line_to(x, y + height);
+ context.stroke()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
+ use cairo::{Context, Format, ImageSurface};
#[test]
fn test_draw_shapes() {
// Just verify the functions don't panic
- let surface = cairo::ImageSurface::create(cairo::Format::Rgb24, 100, 100).unwrap();
- let ctx = cairo::Context::new(&surface).unwrap();
+ let surface = ImageSurface::create(Format::Rgb24, 100, 100).unwrap();
+ let context = Context::new(&surface).unwrap();
- assert!(draw_vertex(&ctx, &Shape::Circle, 0.0, 0.0, 25.0, 25.0).is_ok());
- assert!(draw_vertex(&ctx, &Shape::Square, 0.0, 0.0, 25.0, 25.0).is_ok());
- assert!(draw_vertex(&ctx, &Shape::Triangle, 0.0, 0.0, 25.0, 25.0).is_ok());
- assert!(draw_vertex(&ctx, &Shape::X, 0.0, 0.0, 25.0, 25.0).is_ok());
+ assert!(draw_vertex(&context, &Shape::Circle, 0.0, 0.0, 25.0, 25.0).is_ok());
+ assert!(draw_vertex(&context, &Shape::Square, 0.0, 0.0, 25.0, 25.0).is_ok());
+ assert!(draw_vertex(&context, &Shape::Triangle, 0.0, 0.0, 25.0, 25.0).is_ok());
+ assert!(draw_vertex(&context, &Shape::X, 0.0, 0.0, 25.0, 25.0).is_ok());
}
}
diff --git a/src/smart_positioning.rs b/src/smart_positioning.rs
index 77fd1b6..81a2c3d 100644
--- a/src/smart_positioning.rs
+++ b/src/smart_positioning.rs
@@ -1,5 +1,7 @@
-use crate::geometry::{Line, Rect};
+use crate::geometry::{Line, Rect, SpatialGrid};
use crate::utils::percent_to_pixel;
+
+use cairo::{Context, Error};
use wmap_parser::{Component, Map};
#[derive(Debug, Clone)]
@@ -8,23 +10,30 @@ pub struct LabelPosition {
pub y: f64,
}
-/// Calculate the optimal position for a vertex label with pre-cached collision data
+#[derive(Debug, Clone)]
+pub struct PositioningDimensions {
+ pub map_size: (f64, f64),
+ pub vertex_size: f64,
+ pub font_size: f64,
+}
+
+/// Calculate the optimal position for a vertex label with pre-cached collision data and spatial grid
pub fn calculate_optimal_label_position_cached(
component: &Component,
- map_width: f64,
- map_height: f64,
- vertex_size: f64,
- ctx: &cairo::Context,
- font_size: f64,
+ dimensions: &PositioningDimensions,
+ context: &Context,
all_lines: &[Line],
+ spatial_grid: &SpatialGrid,
note_rects: &[Rect],
-) -> Result<LabelPosition, cairo::Error> {
+) -> Result<LabelPosition, Error> {
+ let map_width = dimensions.map_size.0;
+ let map_height = dimensions.map_size.1;
let vertex_pixel_x = percent_to_pixel(component.coordinates.0, map_width);
let vertex_pixel_y = percent_to_pixel(component.coordinates.1, map_height);
// Calculate label size
- ctx.set_font_size(font_size);
- let extents = ctx.text_extents(&component.label)?;
+ context.set_font_size(dimensions.font_size);
+ let extents = context.text_extents(&component.label)?;
let label_width = extents.width() + 4.0;
let label_height = extents.height() + 2.0;
@@ -32,7 +41,7 @@ pub fn calculate_optimal_label_position_cached(
let candidates = generate_candidate_positions(
vertex_pixel_x,
vertex_pixel_y,
- vertex_size,
+ dimensions.vertex_size,
label_width,
label_height,
);
@@ -46,7 +55,7 @@ pub fn calculate_optimal_label_position_cached(
label_height,
);
- if count_line_collisions(&default_rect, all_lines) == 0
+ if count_line_collisions_with_grid(&default_rect, all_lines, spatial_grid) == 0
&& count_note_collisions(&default_rect, note_rects) == 0
{
return Ok(default_position.clone());
@@ -59,14 +68,14 @@ pub fn calculate_optimal_label_position_cached(
for (index, position) in candidates.iter().enumerate() {
let label_rect = Rect::new(position.x, position.y, label_width, label_height);
- let collision_count = count_line_collisions(&label_rect, all_lines);
+ let collision_count = count_line_collisions_with_grid(&label_rect, all_lines, spatial_grid);
let note_collision_count = count_note_collisions(&label_rect, note_rects);
// Distance from label center to vertex center
let label_center_x = position.x + label_width / 2.0;
let label_center_y = position.y + label_height / 2.0;
- let vertex_center_x = vertex_pixel_x + vertex_size / 2.0;
- let vertex_center_y = vertex_pixel_y + vertex_size / 2.0;
+ let vertex_center_x = vertex_pixel_x + dimensions.vertex_size / 2.0;
+ let vertex_center_y = vertex_pixel_y + dimensions.vertex_size / 2.0;
let distance = ((label_center_x - vertex_center_x).powi(2)
+ (label_center_y - vertex_center_y).powi(2))
.sqrt();
@@ -76,7 +85,7 @@ pub fn calculate_optimal_label_position_cached(
&label_rect,
vertex_pixel_x,
vertex_pixel_y,
- vertex_size,
+ dimensions.vertex_size,
);
// Score: collisions * 100 + note_collisions * 500 + distance * 0.5 + index * 0.1 + ownership
@@ -95,7 +104,6 @@ pub fn calculate_optimal_label_position_cached(
Ok(best_position)
}
-
/// Generate 8 candidate positions around a vertex
fn generate_candidate_positions(
vertex_x: f64,
@@ -156,7 +164,7 @@ pub fn collect_all_lines(
map_width: f64,
map_height: f64,
vertex_size: f64,
- component_map: &std::collections::HashMap<String, (f64, f64)>,
+ component_map: &rustc_hash::FxHashMap<String, (f64, f64)>,
) -> Vec<Line> {
let mut lines = Vec::new();
@@ -216,24 +224,38 @@ pub fn collect_all_lines(
lines
}
+/// Build a spatial grid from a collection of lines for fast collision detection
+pub fn build_spatial_grid(lines: &[Line], map_width: f64, map_height: f64) -> SpatialGrid {
+ // Use a cell size that balances grid overhead with query efficiency
+ // For typical maps, 100x100 pixel cells work well
+ let cell_size = 100.0;
+ let mut grid = SpatialGrid::new(map_width, map_height, cell_size);
+
+ for (idx, line) in lines.iter().enumerate() {
+ grid.insert_line(idx, line);
+ }
+
+ grid
+}
+
/// Collect rectangles for all notes
pub fn collect_note_rects(
map: &Map,
map_width: f64,
map_height: f64,
- ctx: &cairo::Context,
+ context: &Context,
font_size: f64,
-) -> Result<Vec<Rect>, cairo::Error> {
+) -> Result<Vec<Rect>, Error> {
let mut rects = Vec::new();
- ctx.set_font_size(font_size);
+ context.set_font_size(font_size);
for note in &map.notes {
let x = percent_to_pixel(note.coordinates.0, map_width);
let y = percent_to_pixel(note.coordinates.1, map_height);
// Calculate note size (simplified - just use text extents)
- let extents = ctx.text_extents(&note.text)?;
+ let extents = context.text_extents(&note.text)?;
let width = extents.width() + 10.0; // padding
let height = extents.height() + 10.0; // padding
@@ -243,11 +265,15 @@ pub fn collect_note_rects(
Ok(rects)
}
-/// Count how many lines intersect with a label rectangle
-fn count_line_collisions(label_rect: &Rect, lines: &[Line]) -> usize {
- lines
- .iter()
- .filter(|line| crate::geometry::line_intersects_rect(line, label_rect))
+/// Count how many lines intersect with a label rectangle using spatial grid
+fn count_line_collisions_with_grid(
+ label_rect: &Rect,
+ lines: &[Line],
+ spatial_grid: &SpatialGrid,
+) -> usize {
+ spatial_grid
+ .query_rect(label_rect)
+ .filter(|&idx| crate::geometry::line_intersects_rect(&lines[idx], label_rect))
.count()
}
@@ -272,8 +298,9 @@ fn calculate_ownership_penalty_fast(
let vertex_center_x = vertex_x + vertex_size / 2.0;
let vertex_center_y = vertex_y + vertex_size / 2.0;
- let distance =
- ((label_center_x - vertex_center_x).powi(2) + (label_center_y - vertex_center_y).powi(2)).sqrt();
+ let distance = ((label_center_x - vertex_center_x).powi(2)
+ + (label_center_y - vertex_center_y).powi(2))
+ .sqrt();
// Apply penalty if label is too far from its vertex
if distance > 100.0 {
diff --git a/src/stage_type.rs b/src/stage_type.rs
index f3ff056..a003317 100644
--- a/src/stage_type.rs
+++ b/src/stage_type.rs
@@ -28,6 +28,7 @@ pub enum StageType {
impl StageType {
/// Returns the name of this stage type
+ #[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::Activities => "Activities",
@@ -56,6 +57,7 @@ impl StageType {
}
/// Returns the four evolution stage labels for this stage type
+ #[must_use]
pub fn stages(&self) -> [&'static str; 4] {
match self {
Self::Activities => [
diff --git a/src/utils.rs b/src/utils.rs
index b6738d4..8f6699a 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -4,26 +4,6 @@ pub fn percent_to_pixel(percentage: f64, dimension: f64) -> f64 {
(percentage * dimension) / 100.0
}
-/// Parses a hex color string to RGB components (0.0-1.0)
-pub fn parse_color(color: &str) -> (f64, f64, f64) {
- let color = color.trim_start_matches('#');
- if color.len() != 6 {
- return (0.0, 0.0, 0.0); // Default to black on error
- }
-
- let r = u8::from_str_radix(&color[0..2], 16).unwrap_or(0) as f64 / 255.0;
- let g = u8::from_str_radix(&color[2..4], 16).unwrap_or(0) as f64 / 255.0;
- let b = u8::from_str_radix(&color[4..6], 16).unwrap_or(0) as f64 / 255.0;
-
- (r, g, b)
-}
-
-/// Sets the cairo context color from a hex string
-pub fn set_color(ctx: &cairo::Context, color: &str) {
- let (r, g, b) = parse_color(color);
- ctx.set_source_rgb(r, g, b);
-}
-
#[cfg(test)]
mod tests {
use super::*;
@@ -35,19 +15,4 @@ mod tests {
assert_eq!(percent_to_pixel(100.0, 100.0), 100.0);
assert_eq!(percent_to_pixel(25.0, 1300.0), 325.0);
}
-
- #[test]
- fn test_parse_color() {
- // Test white
- assert_eq!(parse_color("#FFFFFF"), (1.0, 1.0, 1.0));
-
- // Test black
- assert_eq!(parse_color("#000000"), (0.0, 0.0, 0.0));
-
- // Test custom color (#0F261F)
- let (r, g, b) = parse_color("#0F261F");
- assert!((r - 0.058823).abs() < 0.001); // 15/255
- assert!((g - 0.149019).abs() < 0.001); // 38/255
- assert!((b - 0.121568).abs() < 0.001); // 31/255
- }
}
diff --git a/tests/integration_test.rs b/tests/integration_test.rs
index 6814429..122ed5a 100644
--- a/tests/integration_test.rs
+++ b/tests/integration_test.rs
@@ -1,4 +1,4 @@
-use wmap_renderer::{render_to_png, render_to_svg, Configuration, StageType};
+use wmap_renderer::{Configuration, StageType, render_to_png, render_to_svg};
#[test]
fn test_render_simple_map_to_png() {