diff options
| -rw-r--r-- | Cargo.lock | 351 | ||||
| -rw-r--r-- | Cargo.toml | 36 | ||||
| -rw-r--r-- | README.md | 52 | ||||
| -rw-r--r-- | benches/rendering.rs | 43 | ||||
| -rw-r--r-- | examples/generate_map.rs | 2 | ||||
| -rw-r--r-- | src/configuration.rs | 152 | ||||
| -rw-r--r-- | src/error.rs | 31 | ||||
| -rw-r--r-- | src/geometry.rs | 102 | ||||
| -rw-r--r-- | src/lib.rs | 102 | ||||
| -rw-r--r-- | src/patterns.rs | 37 | ||||
| -rw-r--r-- | src/renderer.rs | 773 | ||||
| -rw-r--r-- | src/shapes.rs | 97 | ||||
| -rw-r--r-- | src/smart_positioning.rs | 85 | ||||
| -rw-r--r-- | src/stage_type.rs | 2 | ||||
| -rw-r--r-- | src/utils.rs | 35 | ||||
| -rw-r--r-- | tests/integration_test.rs | 2 |
16 files changed, 1044 insertions, 858 deletions
@@ -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", ] @@ -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" @@ -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::*; @@ -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, - ¬e_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, + ¬e_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(¬e.text)?; + let extents = context.text_extents(¬e.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() { |