├── .envrc ├── .github └── workflows │ └── build.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── default.nix ├── flake.lock ├── flake.nix ├── protocols ├── river-control-unstable-v1.xml └── river-layout-v3.xml ├── shell.nix └── src ├── bin └── owm.rs ├── binary.rs ├── derive.rs ├── encoding.rs ├── lib.rs ├── objective ├── adjacent_close.rs ├── area_ratios.rs ├── aspect_ratios.rs ├── center_main.rs ├── consistency.rs ├── gaps.rs ├── mod.rs ├── overlap.rs └── reading_order.rs ├── post_processing.rs ├── rect.rs └── testing.rs /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: cachix/install-nix-action@v12 12 | - name: Building package 13 | run: nix-build . -A defaultPackage.x86_64-linux 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /result 2 | /target 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anstream" 7 | version = "0.3.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" 10 | dependencies = [ 11 | "anstyle", 12 | "anstyle-parse", 13 | "anstyle-query", 14 | "anstyle-wincon", 15 | "colorchoice", 16 | "is-terminal", 17 | "utf8parse", 18 | ] 19 | 20 | [[package]] 21 | name = "anstyle" 22 | version = "1.0.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" 25 | 26 | [[package]] 27 | name = "anstyle-parse" 28 | version = "0.2.1" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" 31 | dependencies = [ 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle-query" 37 | version = "1.0.0" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 40 | dependencies = [ 41 | "windows-sys", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-wincon" 46 | version = "1.0.2" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "c677ab05e09154296dd37acecd46420c17b9713e8366facafa8fc0885167cf4c" 49 | dependencies = [ 50 | "anstyle", 51 | "windows-sys", 52 | ] 53 | 54 | [[package]] 55 | name = "autocfg" 56 | version = "1.1.0" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 59 | 60 | [[package]] 61 | name = "bit-set" 62 | version = "0.5.3" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" 65 | dependencies = [ 66 | "bit-vec", 67 | ] 68 | 69 | [[package]] 70 | name = "bit-vec" 71 | version = "0.6.3" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" 74 | 75 | [[package]] 76 | name = "bitflags" 77 | version = "1.3.2" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 80 | 81 | [[package]] 82 | name = "bitflags" 83 | version = "2.3.3" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" 86 | 87 | [[package]] 88 | name = "blanket" 89 | version = "0.3.0" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "e0b121a9fe0df916e362fb3271088d071159cdf11db0e4182d02152850756eff" 92 | dependencies = [ 93 | "proc-macro2", 94 | "quote", 95 | "syn 2.0.28", 96 | ] 97 | 98 | [[package]] 99 | name = "byteorder" 100 | version = "1.4.3" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 103 | 104 | [[package]] 105 | name = "cc" 106 | version = "1.0.79" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 109 | 110 | [[package]] 111 | name = "cfg-if" 112 | version = "1.0.0" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 115 | 116 | [[package]] 117 | name = "clap" 118 | version = "4.3.21" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "c27cdf28c0f604ba3f512b0c9a409f8de8513e4816705deb0498b627e7c3a3fd" 121 | dependencies = [ 122 | "clap_builder", 123 | "clap_derive", 124 | "once_cell", 125 | ] 126 | 127 | [[package]] 128 | name = "clap_builder" 129 | version = "4.3.21" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "08a9f1ab5e9f01a9b81f202e8562eb9a10de70abf9eaeac1be465c28b75aa4aa" 132 | dependencies = [ 133 | "anstream", 134 | "anstyle", 135 | "clap_lex", 136 | "strsim", 137 | ] 138 | 139 | [[package]] 140 | name = "clap_derive" 141 | version = "4.3.12" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" 144 | dependencies = [ 145 | "heck", 146 | "proc-macro2", 147 | "quote", 148 | "syn 2.0.28", 149 | ] 150 | 151 | [[package]] 152 | name = "clap_lex" 153 | version = "0.5.0" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" 156 | 157 | [[package]] 158 | name = "colorchoice" 159 | version = "1.0.0" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 162 | 163 | [[package]] 164 | name = "convert_case" 165 | version = "0.4.0" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" 168 | 169 | [[package]] 170 | name = "crossbeam-channel" 171 | version = "0.5.8" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" 174 | dependencies = [ 175 | "cfg-if", 176 | "crossbeam-utils", 177 | ] 178 | 179 | [[package]] 180 | name = "crossbeam-deque" 181 | version = "0.8.3" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" 184 | dependencies = [ 185 | "cfg-if", 186 | "crossbeam-epoch", 187 | "crossbeam-utils", 188 | ] 189 | 190 | [[package]] 191 | name = "crossbeam-epoch" 192 | version = "0.9.15" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" 195 | dependencies = [ 196 | "autocfg", 197 | "cfg-if", 198 | "crossbeam-utils", 199 | "memoffset 0.9.0", 200 | "scopeguard", 201 | ] 202 | 203 | [[package]] 204 | name = "crossbeam-utils" 205 | version = "0.8.16" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" 208 | dependencies = [ 209 | "cfg-if", 210 | ] 211 | 212 | [[package]] 213 | name = "derive-getters" 214 | version = "0.3.0" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "7a2c35ab6e03642397cdda1dd58abbc05d418aef8e36297f336d5aba060fe8df" 217 | dependencies = [ 218 | "proc-macro2", 219 | "quote", 220 | "syn 1.0.109", 221 | ] 222 | 223 | [[package]] 224 | name = "derive_more" 225 | version = "0.99.17" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" 228 | dependencies = [ 229 | "convert_case", 230 | "proc-macro2", 231 | "quote", 232 | "rustc_version", 233 | "syn 1.0.109", 234 | ] 235 | 236 | [[package]] 237 | name = "dlib" 238 | version = "0.5.2" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" 241 | dependencies = [ 242 | "libloading", 243 | ] 244 | 245 | [[package]] 246 | name = "downcast-rs" 247 | version = "1.2.0" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" 250 | 251 | [[package]] 252 | name = "either" 253 | version = "1.9.0" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" 256 | 257 | [[package]] 258 | name = "errno" 259 | version = "0.3.2" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" 262 | dependencies = [ 263 | "errno-dragonfly", 264 | "libc", 265 | "windows-sys", 266 | ] 267 | 268 | [[package]] 269 | name = "errno-dragonfly" 270 | version = "0.1.2" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 273 | dependencies = [ 274 | "cc", 275 | "libc", 276 | ] 277 | 278 | [[package]] 279 | name = "fastrand" 280 | version = "2.0.0" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" 283 | 284 | [[package]] 285 | name = "fnv" 286 | version = "1.0.7" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 289 | 290 | [[package]] 291 | name = "getrandom" 292 | version = "0.2.10" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" 295 | dependencies = [ 296 | "cfg-if", 297 | "libc", 298 | "wasi", 299 | ] 300 | 301 | [[package]] 302 | name = "heck" 303 | version = "0.4.1" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 306 | 307 | [[package]] 308 | name = "hermit-abi" 309 | version = "0.3.2" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" 312 | 313 | [[package]] 314 | name = "io-lifetimes" 315 | version = "1.0.11" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" 318 | dependencies = [ 319 | "hermit-abi", 320 | "libc", 321 | "windows-sys", 322 | ] 323 | 324 | [[package]] 325 | name = "is-terminal" 326 | version = "0.4.9" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" 329 | dependencies = [ 330 | "hermit-abi", 331 | "rustix", 332 | "windows-sys", 333 | ] 334 | 335 | [[package]] 336 | name = "itertools" 337 | version = "0.11.0" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" 340 | dependencies = [ 341 | "either", 342 | ] 343 | 344 | [[package]] 345 | name = "lazy_static" 346 | version = "1.4.0" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 349 | 350 | [[package]] 351 | name = "libc" 352 | version = "0.2.147" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" 355 | 356 | [[package]] 357 | name = "libloading" 358 | version = "0.8.0" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "d580318f95776505201b28cf98eb1fa5e4be3b689633ba6a3e6cd880ff22d8cb" 361 | dependencies = [ 362 | "cfg-if", 363 | "windows-sys", 364 | ] 365 | 366 | [[package]] 367 | name = "libm" 368 | version = "0.2.7" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" 371 | 372 | [[package]] 373 | name = "linux-raw-sys" 374 | version = "0.4.5" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" 377 | 378 | [[package]] 379 | name = "log" 380 | version = "0.4.19" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" 383 | 384 | [[package]] 385 | name = "matrixmultiply" 386 | version = "0.3.7" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "090126dc04f95dc0d1c1c91f61bdd474b3930ca064c1edc8a849da2c6cbe1e77" 389 | dependencies = [ 390 | "autocfg", 391 | "rawpointer", 392 | ] 393 | 394 | [[package]] 395 | name = "memchr" 396 | version = "2.5.0" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 399 | 400 | [[package]] 401 | name = "memoffset" 402 | version = "0.7.1" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" 405 | dependencies = [ 406 | "autocfg", 407 | ] 408 | 409 | [[package]] 410 | name = "memoffset" 411 | version = "0.9.0" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" 414 | dependencies = [ 415 | "autocfg", 416 | ] 417 | 418 | [[package]] 419 | name = "ndarray" 420 | version = "0.15.6" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" 423 | dependencies = [ 424 | "matrixmultiply", 425 | "num-complex", 426 | "num-integer", 427 | "num-traits", 428 | "rawpointer", 429 | ] 430 | 431 | [[package]] 432 | name = "ndarray-rand" 433 | version = "0.14.0" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "65608f937acc725f5b164dcf40f4f0bc5d67dc268ab8a649d3002606718c4588" 436 | dependencies = [ 437 | "ndarray", 438 | "rand", 439 | "rand_distr", 440 | ] 441 | 442 | [[package]] 443 | name = "nix" 444 | version = "0.26.2" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" 447 | dependencies = [ 448 | "bitflags 1.3.2", 449 | "cfg-if", 450 | "libc", 451 | "memoffset 0.7.1", 452 | "static_assertions", 453 | ] 454 | 455 | [[package]] 456 | name = "num-complex" 457 | version = "0.4.3" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d" 460 | dependencies = [ 461 | "num-traits", 462 | ] 463 | 464 | [[package]] 465 | name = "num-integer" 466 | version = "0.1.45" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 469 | dependencies = [ 470 | "autocfg", 471 | "num-traits", 472 | ] 473 | 474 | [[package]] 475 | name = "num-traits" 476 | version = "0.2.16" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" 479 | dependencies = [ 480 | "autocfg", 481 | "libm", 482 | ] 483 | 484 | [[package]] 485 | name = "num_cpus" 486 | version = "1.16.0" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 489 | dependencies = [ 490 | "hermit-abi", 491 | "libc", 492 | ] 493 | 494 | [[package]] 495 | name = "once_cell" 496 | version = "1.18.0" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 499 | 500 | [[package]] 501 | name = "optimal" 502 | version = "0.1.0" 503 | source = "git+https://github.com/justinlovinger/optimal-rs.git#2c1a65a7663ce822d625e1d03e1f764dde473a66" 504 | dependencies = [ 505 | "blanket", 506 | "derive-getters", 507 | "derive_more", 508 | "lazy_static", 509 | "ndarray", 510 | "ndarray-rand", 511 | "num-traits", 512 | "once_cell", 513 | "paste", 514 | "rand", 515 | "rand_xoshiro", 516 | "replace_with", 517 | "streaming-iterator", 518 | "thiserror", 519 | ] 520 | 521 | [[package]] 522 | name = "owm" 523 | version = "0.1.0" 524 | dependencies = [ 525 | "clap", 526 | "derive_more", 527 | "itertools", 528 | "ndarray", 529 | "num-traits", 530 | "once_cell", 531 | "optimal", 532 | "paste", 533 | "proptest", 534 | "rand", 535 | "rand_xoshiro", 536 | "rayon", 537 | "test-strategy", 538 | "thiserror", 539 | "wayland-client", 540 | "wayland-scanner", 541 | ] 542 | 543 | [[package]] 544 | name = "paste" 545 | version = "1.0.14" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" 548 | 549 | [[package]] 550 | name = "pkg-config" 551 | version = "0.3.27" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" 554 | 555 | [[package]] 556 | name = "ppv-lite86" 557 | version = "0.2.17" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 560 | 561 | [[package]] 562 | name = "proc-macro2" 563 | version = "1.0.66" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" 566 | dependencies = [ 567 | "unicode-ident", 568 | ] 569 | 570 | [[package]] 571 | name = "proptest" 572 | version = "1.2.0" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "4e35c06b98bf36aba164cc17cb25f7e232f5c4aeea73baa14b8a9f0d92dbfa65" 575 | dependencies = [ 576 | "bit-set", 577 | "bitflags 1.3.2", 578 | "byteorder", 579 | "lazy_static", 580 | "num-traits", 581 | "rand", 582 | "rand_chacha", 583 | "rand_xorshift", 584 | "regex-syntax", 585 | "rusty-fork", 586 | "tempfile", 587 | "unarray", 588 | ] 589 | 590 | [[package]] 591 | name = "quick-error" 592 | version = "1.2.3" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 595 | 596 | [[package]] 597 | name = "quick-xml" 598 | version = "0.28.2" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" 601 | dependencies = [ 602 | "memchr", 603 | ] 604 | 605 | [[package]] 606 | name = "quote" 607 | version = "1.0.32" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" 610 | dependencies = [ 611 | "proc-macro2", 612 | ] 613 | 614 | [[package]] 615 | name = "rand" 616 | version = "0.8.5" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 619 | dependencies = [ 620 | "libc", 621 | "rand_chacha", 622 | "rand_core", 623 | ] 624 | 625 | [[package]] 626 | name = "rand_chacha" 627 | version = "0.3.1" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 630 | dependencies = [ 631 | "ppv-lite86", 632 | "rand_core", 633 | ] 634 | 635 | [[package]] 636 | name = "rand_core" 637 | version = "0.6.4" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 640 | dependencies = [ 641 | "getrandom", 642 | ] 643 | 644 | [[package]] 645 | name = "rand_distr" 646 | version = "0.4.3" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" 649 | dependencies = [ 650 | "num-traits", 651 | "rand", 652 | ] 653 | 654 | [[package]] 655 | name = "rand_xorshift" 656 | version = "0.3.0" 657 | source = "registry+https://github.com/rust-lang/crates.io-index" 658 | checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" 659 | dependencies = [ 660 | "rand_core", 661 | ] 662 | 663 | [[package]] 664 | name = "rand_xoshiro" 665 | version = "0.6.0" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" 668 | dependencies = [ 669 | "rand_core", 670 | ] 671 | 672 | [[package]] 673 | name = "rawpointer" 674 | version = "0.2.1" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" 677 | 678 | [[package]] 679 | name = "rayon" 680 | version = "1.7.0" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" 683 | dependencies = [ 684 | "either", 685 | "rayon-core", 686 | ] 687 | 688 | [[package]] 689 | name = "rayon-core" 690 | version = "1.11.0" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" 693 | dependencies = [ 694 | "crossbeam-channel", 695 | "crossbeam-deque", 696 | "crossbeam-utils", 697 | "num_cpus", 698 | ] 699 | 700 | [[package]] 701 | name = "redox_syscall" 702 | version = "0.3.5" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" 705 | dependencies = [ 706 | "bitflags 1.3.2", 707 | ] 708 | 709 | [[package]] 710 | name = "regex-syntax" 711 | version = "0.6.29" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 714 | 715 | [[package]] 716 | name = "replace_with" 717 | version = "0.1.7" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "e3a8614ee435691de62bcffcf4a66d91b3594bf1428a5722e79103249a095690" 720 | 721 | [[package]] 722 | name = "rustc_version" 723 | version = "0.4.0" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 726 | dependencies = [ 727 | "semver", 728 | ] 729 | 730 | [[package]] 731 | name = "rustix" 732 | version = "0.38.6" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "1ee020b1716f0a80e2ace9b03441a749e402e86712f15f16fe8a8f75afac732f" 735 | dependencies = [ 736 | "bitflags 2.3.3", 737 | "errno", 738 | "libc", 739 | "linux-raw-sys", 740 | "windows-sys", 741 | ] 742 | 743 | [[package]] 744 | name = "rusty-fork" 745 | version = "0.3.0" 746 | source = "registry+https://github.com/rust-lang/crates.io-index" 747 | checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" 748 | dependencies = [ 749 | "fnv", 750 | "quick-error", 751 | "tempfile", 752 | "wait-timeout", 753 | ] 754 | 755 | [[package]] 756 | name = "scoped-tls" 757 | version = "1.0.1" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" 760 | 761 | [[package]] 762 | name = "scopeguard" 763 | version = "1.2.0" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 766 | 767 | [[package]] 768 | name = "semver" 769 | version = "1.0.18" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" 772 | 773 | [[package]] 774 | name = "smallvec" 775 | version = "1.11.0" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" 778 | 779 | [[package]] 780 | name = "static_assertions" 781 | version = "1.1.0" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 784 | 785 | [[package]] 786 | name = "streaming-iterator" 787 | version = "0.1.9" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" 790 | 791 | [[package]] 792 | name = "strsim" 793 | version = "0.10.0" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 796 | 797 | [[package]] 798 | name = "structmeta" 799 | version = "0.2.0" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "78ad9e09554f0456d67a69c1584c9798ba733a5b50349a6c0d0948710523922d" 802 | dependencies = [ 803 | "proc-macro2", 804 | "quote", 805 | "structmeta-derive", 806 | "syn 2.0.28", 807 | ] 808 | 809 | [[package]] 810 | name = "structmeta-derive" 811 | version = "0.2.0" 812 | source = "registry+https://github.com/rust-lang/crates.io-index" 813 | checksum = "a60bcaff7397072dca0017d1db428e30d5002e00b6847703e2e42005c95fbe00" 814 | dependencies = [ 815 | "proc-macro2", 816 | "quote", 817 | "syn 2.0.28", 818 | ] 819 | 820 | [[package]] 821 | name = "syn" 822 | version = "1.0.109" 823 | source = "registry+https://github.com/rust-lang/crates.io-index" 824 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 825 | dependencies = [ 826 | "proc-macro2", 827 | "quote", 828 | "unicode-ident", 829 | ] 830 | 831 | [[package]] 832 | name = "syn" 833 | version = "2.0.28" 834 | source = "registry+https://github.com/rust-lang/crates.io-index" 835 | checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" 836 | dependencies = [ 837 | "proc-macro2", 838 | "quote", 839 | "unicode-ident", 840 | ] 841 | 842 | [[package]] 843 | name = "tempfile" 844 | version = "3.7.0" 845 | source = "registry+https://github.com/rust-lang/crates.io-index" 846 | checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" 847 | dependencies = [ 848 | "cfg-if", 849 | "fastrand", 850 | "redox_syscall", 851 | "rustix", 852 | "windows-sys", 853 | ] 854 | 855 | [[package]] 856 | name = "test-strategy" 857 | version = "0.3.1" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "b8361c808554228ad09bfed70f5c823caf8a3450b6881cc3a38eb57e8c08c1d9" 860 | dependencies = [ 861 | "proc-macro2", 862 | "quote", 863 | "structmeta", 864 | "syn 2.0.28", 865 | ] 866 | 867 | [[package]] 868 | name = "thiserror" 869 | version = "1.0.46" 870 | source = "registry+https://github.com/rust-lang/crates.io-index" 871 | checksum = "d9207952ae1a003f42d3d5e892dac3c6ba42aa6ac0c79a6a91a2b5cb4253e75c" 872 | dependencies = [ 873 | "thiserror-impl", 874 | ] 875 | 876 | [[package]] 877 | name = "thiserror-impl" 878 | version = "1.0.46" 879 | source = "registry+https://github.com/rust-lang/crates.io-index" 880 | checksum = "f1728216d3244de4f14f14f8c15c79be1a7c67867d28d69b719690e2a19fb445" 881 | dependencies = [ 882 | "proc-macro2", 883 | "quote", 884 | "syn 2.0.28", 885 | ] 886 | 887 | [[package]] 888 | name = "unarray" 889 | version = "0.1.4" 890 | source = "registry+https://github.com/rust-lang/crates.io-index" 891 | checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" 892 | 893 | [[package]] 894 | name = "unicode-ident" 895 | version = "1.0.11" 896 | source = "registry+https://github.com/rust-lang/crates.io-index" 897 | checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" 898 | 899 | [[package]] 900 | name = "utf8parse" 901 | version = "0.2.1" 902 | source = "registry+https://github.com/rust-lang/crates.io-index" 903 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 904 | 905 | [[package]] 906 | name = "wait-timeout" 907 | version = "0.2.0" 908 | source = "registry+https://github.com/rust-lang/crates.io-index" 909 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 910 | dependencies = [ 911 | "libc", 912 | ] 913 | 914 | [[package]] 915 | name = "wasi" 916 | version = "0.11.0+wasi-snapshot-preview1" 917 | source = "registry+https://github.com/rust-lang/crates.io-index" 918 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 919 | 920 | [[package]] 921 | name = "wayland-backend" 922 | version = "0.1.2" 923 | source = "registry+https://github.com/rust-lang/crates.io-index" 924 | checksum = "41b48e27457e8da3b2260ac60d0a94512f5cba36448679f3747c0865b7893ed8" 925 | dependencies = [ 926 | "cc", 927 | "downcast-rs", 928 | "io-lifetimes", 929 | "nix", 930 | "scoped-tls", 931 | "smallvec", 932 | "wayland-sys", 933 | ] 934 | 935 | [[package]] 936 | name = "wayland-client" 937 | version = "0.30.2" 938 | source = "registry+https://github.com/rust-lang/crates.io-index" 939 | checksum = "489c9654770f674fc7e266b3c579f4053d7551df0ceb392f153adb1f9ed06ac8" 940 | dependencies = [ 941 | "bitflags 1.3.2", 942 | "nix", 943 | "wayland-backend", 944 | "wayland-scanner", 945 | ] 946 | 947 | [[package]] 948 | name = "wayland-scanner" 949 | version = "0.30.1" 950 | source = "registry+https://github.com/rust-lang/crates.io-index" 951 | checksum = "b9b873b257fbc32ec909c0eb80dea312076a67014e65e245f5eb69a6b8ab330e" 952 | dependencies = [ 953 | "proc-macro2", 954 | "quick-xml", 955 | "quote", 956 | ] 957 | 958 | [[package]] 959 | name = "wayland-sys" 960 | version = "0.30.1" 961 | source = "registry+https://github.com/rust-lang/crates.io-index" 962 | checksum = "96b2a02ac608e07132978689a6f9bf4214949c85998c247abadd4f4129b1aa06" 963 | dependencies = [ 964 | "dlib", 965 | "log", 966 | "pkg-config", 967 | ] 968 | 969 | [[package]] 970 | name = "windows-sys" 971 | version = "0.48.0" 972 | source = "registry+https://github.com/rust-lang/crates.io-index" 973 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 974 | dependencies = [ 975 | "windows-targets", 976 | ] 977 | 978 | [[package]] 979 | name = "windows-targets" 980 | version = "0.48.1" 981 | source = "registry+https://github.com/rust-lang/crates.io-index" 982 | checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" 983 | dependencies = [ 984 | "windows_aarch64_gnullvm", 985 | "windows_aarch64_msvc", 986 | "windows_i686_gnu", 987 | "windows_i686_msvc", 988 | "windows_x86_64_gnu", 989 | "windows_x86_64_gnullvm", 990 | "windows_x86_64_msvc", 991 | ] 992 | 993 | [[package]] 994 | name = "windows_aarch64_gnullvm" 995 | version = "0.48.0" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 998 | 999 | [[package]] 1000 | name = "windows_aarch64_msvc" 1001 | version = "0.48.0" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 1004 | 1005 | [[package]] 1006 | name = "windows_i686_gnu" 1007 | version = "0.48.0" 1008 | source = "registry+https://github.com/rust-lang/crates.io-index" 1009 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 1010 | 1011 | [[package]] 1012 | name = "windows_i686_msvc" 1013 | version = "0.48.0" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 1016 | 1017 | [[package]] 1018 | name = "windows_x86_64_gnu" 1019 | version = "0.48.0" 1020 | source = "registry+https://github.com/rust-lang/crates.io-index" 1021 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 1022 | 1023 | [[package]] 1024 | name = "windows_x86_64_gnullvm" 1025 | version = "0.48.0" 1026 | source = "registry+https://github.com/rust-lang/crates.io-index" 1027 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 1028 | 1029 | [[package]] 1030 | name = "windows_x86_64_msvc" 1031 | version = "0.48.0" 1032 | source = "registry+https://github.com/rust-lang/crates.io-index" 1033 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 1034 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "owm" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Justin Lovinger"] 6 | description = "An experimental River layout generator using mathematical optimization to invent layouts on-the-fly" 7 | repository = "https://github.com/justinlovinger/owm" 8 | readme = "README.md" 9 | keywords = ["optimization", "river", "window-manager"] 10 | categories = ["command-line-utilities"] 11 | license = "MIT" 12 | 13 | [profile.dev] 14 | # Generating layouts is significantly slower than compiling. 15 | opt-level = 3 16 | 17 | [profile.test] 18 | opt-level = 0 19 | 20 | [dependencies] 21 | clap = { version = "4.3.21", features = ["derive"] } 22 | derive_more = "0.99.17" 23 | itertools = "0.11.0" 24 | ndarray = "0.15.6" 25 | num-traits = "0.2.16" 26 | once_cell = "1.18.0" 27 | optimal = { git = "https://github.com/justinlovinger/optimal-rs.git" } 28 | paste = "1.0.14" 29 | rand = "0.8.5" 30 | rand_xoshiro = "0.6.0" 31 | rayon = "1.7.0" 32 | thiserror = "1.0.46" 33 | wayland-client = "0.30.2" 34 | wayland-scanner = "0.30.1" 35 | 36 | [dev-dependencies] 37 | proptest = "1.2.0" 38 | test-strategy = "0.3.1" 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Justin Lovinger 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Workflow Status](https://github.com/justinlovinger/owm/workflows/build/badge.svg)](https://github.com/justinlovinger/owm/actions?query=workflow%3A%22build%22) 2 | 3 | # owm 4 | 5 | An experimental [River](https://github.com/riverwm/river) layout generator 6 | using mathematical optimization 7 | to invent layouts 8 | on-the-fly. 9 | 10 | ## Building 11 | 12 | Run `cargo build --release` or `nix build`. 13 | 14 | ## Usage 15 | 16 | Add 17 | 18 | ``` 19 | riverctl default-layout owm 20 | owm & 21 | ``` 22 | 23 | to your River `init`. 24 | 25 | See `owm --help` for configuration options. 26 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | (import ( 2 | fetchTarball { 3 | url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz"; 4 | sha256 = "0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2"; } 5 | ) { 6 | src = ./.; 7 | }).defaultNix 8 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "naersk": { 4 | "inputs": { 5 | "nixpkgs": "nixpkgs" 6 | }, 7 | "locked": { 8 | "lastModified": 1690373729, 9 | "narHash": "sha256-e136hTT7LqQ2QjOTZQMW+jnsevWwBpMj78u6FRUsH9I=", 10 | "owner": "nix-community", 11 | "repo": "naersk", 12 | "rev": "d9a33d69a9c421d64c8d925428864e93be895dcc", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "nix-community", 17 | "ref": "master", 18 | "repo": "naersk", 19 | "type": "github" 20 | } 21 | }, 22 | "nixpkgs": { 23 | "locked": { 24 | "lastModified": 1690593349, 25 | "narHash": "sha256-i6jdORO+YiP19pFNeR7oYIIwmzQvdxwNO+BmtATcYpA=", 26 | "owner": "NixOS", 27 | "repo": "nixpkgs", 28 | "rev": "11cf5e1c74fe6892e860afeeaf3bfb84fdb7b1c3", 29 | "type": "github" 30 | }, 31 | "original": { 32 | "id": "nixpkgs", 33 | "type": "indirect" 34 | } 35 | }, 36 | "nixpkgs_2": { 37 | "locked": { 38 | "lastModified": 1690593349, 39 | "narHash": "sha256-i6jdORO+YiP19pFNeR7oYIIwmzQvdxwNO+BmtATcYpA=", 40 | "owner": "NixOS", 41 | "repo": "nixpkgs", 42 | "rev": "11cf5e1c74fe6892e860afeeaf3bfb84fdb7b1c3", 43 | "type": "github" 44 | }, 45 | "original": { 46 | "owner": "NixOS", 47 | "ref": "nixpkgs-unstable", 48 | "repo": "nixpkgs", 49 | "type": "github" 50 | } 51 | }, 52 | "root": { 53 | "inputs": { 54 | "naersk": "naersk", 55 | "nixpkgs": "nixpkgs_2", 56 | "utils": "utils" 57 | } 58 | }, 59 | "systems": { 60 | "locked": { 61 | "lastModified": 1681028828, 62 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 63 | "owner": "nix-systems", 64 | "repo": "default", 65 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 66 | "type": "github" 67 | }, 68 | "original": { 69 | "owner": "nix-systems", 70 | "repo": "default", 71 | "type": "github" 72 | } 73 | }, 74 | "utils": { 75 | "inputs": { 76 | "systems": "systems" 77 | }, 78 | "locked": { 79 | "lastModified": 1689068808, 80 | "narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=", 81 | "owner": "numtide", 82 | "repo": "flake-utils", 83 | "rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4", 84 | "type": "github" 85 | }, 86 | "original": { 87 | "owner": "numtide", 88 | "repo": "flake-utils", 89 | "type": "github" 90 | } 91 | } 92 | }, 93 | "root": "root", 94 | "version": 7 95 | } 96 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | naersk.url = "github:nix-community/naersk/master"; 4 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 5 | utils.url = "github:numtide/flake-utils"; 6 | }; 7 | 8 | outputs = { 9 | self, 10 | naersk, 11 | nixpkgs, 12 | utils, 13 | }: 14 | utils.lib.eachDefaultSystem (system: let 15 | pkgs = import nixpkgs {inherit system;}; 16 | naersk-lib = pkgs.callPackage naersk {}; 17 | in { 18 | defaultPackage = naersk-lib.buildPackage { 19 | src = ./.; 20 | doCheck = true; 21 | }; 22 | devShell = with pkgs; 23 | mkShell { 24 | buildInputs = [ 25 | cargo 26 | rust-analyzer 27 | rustc 28 | rustfmt 29 | rustPackages.clippy 30 | ]; 31 | RUST_SRC_PATH = rustPlatform.rustLibSrc; 32 | PROPTEST_DISABLE_FAILURE_PERSISTENCE = 1; 33 | }; 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /protocols/river-control-unstable-v1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright 2020 The River Developers 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | 18 | 19 | 20 | 21 | This interface allows clients to run compositor commands and receive a 22 | success/failure response with output or a failure message respectively. 23 | 24 | Each command is built up in a series of add_argument requests and 25 | executed with a run_command request. The first argument is the command 26 | to be run. 27 | 28 | A complete list of commands should be made available in the man page of 29 | the compositor. 30 | 31 | 32 | 33 | 34 | This request indicates that the client will not use the 35 | river_control object any more. Objects that have been created 36 | through this instance are not affected. 37 | 38 | 39 | 40 | 41 | 42 | Arguments are stored by the server in the order they were sent until 43 | the run_command request is made. 44 | 45 | 46 | 47 | 48 | 49 | 50 | Execute the command built up using the add_argument request for the 51 | given seat. 52 | 53 | 54 | 56 | 57 | 58 | 59 | 60 | 61 | This object is created by the run_command request. Exactly one of the 62 | success or failure events will be sent. This object will be destroyed 63 | by the compositor after one of the events is sent. 64 | 65 | 66 | 67 | 68 | Sent when the command has been successfully received and executed by 69 | the compositor. Some commands may produce output, in which case the 70 | output argument will be a non-empty string. 71 | 72 | 73 | 74 | 75 | 76 | 77 | Sent when the command could not be carried out. This could be due to 78 | sending a non-existent command, no command, not enough arguments, too 79 | many arguments, invalid arguments, etc. 80 | 81 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /protocols/river-layout-v3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright 2020-2021 The River Developers 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | 18 | 19 | 20 | This protocol specifies a way for clients to propose arbitrary positions 21 | and dimensions for a set of views on a specific output of a compositor 22 | through the river_layout_v3 object. 23 | 24 | Layouts are a strictly linear list of views, the position and dimensions 25 | of which are supplied by the client. Any complex underlying data structure 26 | a client may use when generating the layout is lost in transmission. This 27 | is an intentional limitation. 28 | 29 | Additionally, this protocol allows the compositor to deliver arbitrary 30 | user-provided commands associated with a layout to clients. A client 31 | may use these commands to implement runtime configuration/control, or 32 | may ignore them entirely. How the user provides these commands to the 33 | compositor is not specified by this protocol and left to compositor policy. 34 | 35 | Warning! The protocol described in this file is currently in the 36 | testing phase. Backward compatible changes may be added together with 37 | the corresponding interface version bump. Backward incompatible changes 38 | can only be done by creating a new major version of the extension. 39 | 40 | 41 | 42 | 43 | A global factory for river_layout_v3 objects. 44 | 45 | 46 | 47 | 48 | This request indicates that the client will not use the 49 | river_layout_manager object any more. Objects that have been created 50 | through this instance are not affected. 51 | 52 | 53 | 54 | 55 | 56 | This creates a new river_layout_v3 object for the given wl_output. 57 | 58 | All layout related communication is done through this interface. 59 | 60 | The namespace is used by the compositor to decide which river_layout_v3 61 | object will receive layout demands for the output. 62 | 63 | The namespace is required to be be unique per-output. Furthermore, 64 | two separate clients may not share a namespace on separate outputs. If 65 | these conditions are not upheld, the the namespace_in_use event will 66 | be sent directly after creation of the river_layout_v3 object. 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | This interface allows clients to receive layout demands from the 77 | compositor for a specific output and subsequently propose positions and 78 | dimensions of individual views. 79 | 80 | 81 | 82 | 84 | 86 | 87 | 88 | 89 | 90 | This request indicates that the client will not use the river_layout_v3 91 | object any more. 92 | 93 | 94 | 95 | 96 | 97 | After this event is sent, all requests aside from the destroy event 98 | will be ignored by the server. If the client wishes to try again with 99 | a different namespace they must create a new river_layout_v3 object. 100 | 101 | 102 | 103 | 104 | 105 | The compositor sends this event to inform the client that it requires a 106 | layout for a set of views. 107 | 108 | The usable width and height indicate the space in which the client 109 | can safely position views without interfering with desktop widgets 110 | such as panels. 111 | 112 | The serial of this event is used to identify subsequent requests as 113 | belonging to this layout demand. Beware that the client might need 114 | to handle multiple layout demands at the same time. 115 | 116 | The server will ignore responses to all but the most recent layout 117 | demand. Thus, clients are only required to respond to the most recent 118 | layout_demand received. If a newer layout_demand is received before 119 | the client has finished responding to an old demand, the client should 120 | abort work on the old demand as any further work would be wasted. 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | This request proposes a size and position for a view in the layout demand 132 | with matching serial. 133 | 134 | A client must send this request for every view that is part of the 135 | layout demand. The number of views in the layout is given by the 136 | view_count argument of the layout_demand event. Pushing too many or 137 | too few view dimensions is a protocol error. 138 | 139 | The x and y coordinates are relative to the usable area of the output, 140 | with (0,0) as the top left corner. 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | This request indicates that the client is done pushing dimensions 152 | and the compositor may apply the layout. This completes the layout 153 | demand with matching serial, any other requests sent with the serial 154 | are a protocol error. 155 | 156 | The layout_name argument is a user-facing name or short description 157 | of the layout that is being committed. The compositor may for example 158 | display this on a status bar, though what exactly is done with it is 159 | left to the compositor's discretion. 160 | 161 | The compositor is free to use this proposed layout however it chooses, 162 | including ignoring it. 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | This event informs the client of a command sent to it by the user. 171 | 172 | The semantic meaning of the command is left for the client to 173 | decide. It is also free to ignore it entirely if it so chooses. 174 | 175 | A layout_demand will be sent after this event if the compositor is 176 | currently using this layout object to arrange the output. 177 | 178 | If version 2 or higher of the river_layout_v3 object is bound, the 179 | user_command_tags event is guaranteed to be sent directly before the 180 | user_command event. 181 | 182 | 183 | 184 | 185 | 186 | 187 | If version 2 or higher of the river_layout_v3 object is bound, this 188 | event will be sent directly before every user_command event. This allows 189 | layout generators to be aware of the active tags when a user command is 190 | sent. This is necessary for generators wanting to keep settings on a 191 | per-tag basis. 192 | 193 | 194 | 195 | 196 | 197 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | (import ( 2 | fetchTarball { 3 | url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz"; 4 | sha256 = "0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2"; } 5 | ) { 6 | src = ./.; 7 | }).shellNix 8 | -------------------------------------------------------------------------------- /src/bin/owm.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroUsize; 2 | use std::str::FromStr; 3 | use std::sync::{Arc, Mutex}; 4 | 5 | use clap::Parser; 6 | use owm::{AreaRatio, AspectRatio, LayoutGen, Size, Status, Weight, Weights}; 7 | use wayland_client::protocol::wl_seat::WlSeat; 8 | use wayland_client::Connection; 9 | use wayland_client::{ 10 | backend::ObjectId, 11 | protocol::{ 12 | wl_output::{self, WlOutput}, 13 | wl_registry::{self, WlRegistry}, 14 | }, 15 | Dispatch, Proxy, 16 | }; 17 | 18 | use crate::protocol::{ 19 | river_layout_manager_v3::RiverLayoutManagerV3, 20 | river_layout_v3::{self, RiverLayoutV3}, 21 | zriver_command_callback_v1::ZriverCommandCallbackV1, 22 | zriver_control_v1::ZriverControlV1, 23 | }; 24 | 25 | #[derive(Parser)] 26 | #[clap(author, version, about, long_about = None)] 27 | struct Args { 28 | /// River namespace for this instance of the layout generator. 29 | /// Multiple instances can run simultaneously 30 | /// using different namespaces. 31 | /// Instances can be switched between 32 | /// using `riverctl default-layout NAMESPACE` 33 | /// or `riverctl output-layout NAMESPACE`. 34 | #[arg(long, value_name = "NAMESPACE", default_value = "owm")] 35 | namespace: String, 36 | 37 | #[arg(long, value_name = "NON_ZERO_UINT", default_value_t = NonZeroUsize::new(320).unwrap())] 38 | min_width: NonZeroUsize, 39 | 40 | #[arg(long, value_name = "NON_ZERO_UINT", default_value_t = NonZeroUsize::new(180).unwrap())] 41 | min_height: NonZeroUsize, 42 | 43 | #[arg(long, value_name = "NON_ZERO_UINT", value_parser = non_zero_usize_option_parser, default_value = "1920")] 44 | max_width: std::option::Option, 45 | 46 | #[arg(long, value_name = "NON_ZERO_UINT", value_parser = non_zero_usize_option_parser, default_value = "")] 47 | max_height: std::option::Option, 48 | 49 | /// Set to border thickness 50 | /// to fully overlap borders. 51 | #[arg(long, value_name = "UINT", default_value = "0")] 52 | overlap_borders_by: usize, 53 | 54 | /// Importance of "minimize gaps" objective. 55 | #[arg(long, value_name = "WEIGHT", default_value_t = Weight::new(5.0).unwrap())] 56 | gaps_weight: Weight, 57 | 58 | /// Importance of "minimize overlap" objective. 59 | #[arg(long, value_name = "WEIGHT", default_value_t = Weight::new(6.0).unwrap())] 60 | overlap_weight: Weight, 61 | 62 | /// Desired area ratios between each window and the next. 63 | /// 64 | /// Values are comma-separated. 65 | /// Last value is repeated for further pairs. 66 | /// Each value must be >= 1. 67 | #[arg( 68 | long, 69 | value_name = "RATIOS", 70 | value_delimiter = ',', 71 | default_value = "3,2,1" 72 | )] 73 | area_ratios: Vec, 74 | 75 | /// Importance of "maintain area ratios" objective. 76 | #[arg(long, value_name = "WEIGHT", default_value_t = Weight::new(1.5).unwrap())] 77 | area_ratios_weight: Weight, 78 | 79 | /// Desired aspect ratios of windows. 80 | /// 81 | /// Values are comma-separated. 82 | /// Last value is repeated for further windows. 83 | /// Each value must be > 0. 84 | #[arg( 85 | long, 86 | value_name = "RATIOS", 87 | value_delimiter = ',', 88 | default_value = "1.77777" 89 | )] 90 | aspect_ratios: Vec, 91 | 92 | /// Importance of "maintain aspect ratios" objective. 93 | #[arg(long, value_name = "WEIGHT", default_value_t = Weight::new(3.0).unwrap())] 94 | aspect_ratios_weight: Weight, 95 | 96 | /// Importance of "place adjacent close" objective. 97 | #[arg(long, value_name = "WEIGHT", default_value_t = Weight::new(0.5).unwrap())] 98 | adjacent_close_weight: Weight, 99 | 100 | /// Importance of "place in reading order" objective. 101 | #[arg(long, value_name = "WEIGHT", default_value_t = Weight::new(0.5).unwrap())] 102 | reading_order_weight: Weight, 103 | 104 | /// Importance of "center main" objective. 105 | #[arg(long, value_name = "WEIGHT", default_value_t = Weight::new(1.5).unwrap())] 106 | center_main_weight: Weight, 107 | 108 | /// Importance of keeping layout consistent 109 | /// from one number of windows 110 | /// to the next. 111 | #[arg(long, value_name = "WEIGHT", default_value_t = Weight::new(1.0).unwrap())] 112 | consistency_weight: Weight, 113 | } 114 | 115 | fn non_zero_usize_option_parser( 116 | s: &str, 117 | ) -> Result, ::Err> { 118 | option_parser(s) 119 | } 120 | 121 | fn option_parser(s: &str) -> Result, ::Err> 122 | where 123 | T: FromStr, 124 | { 125 | if s.is_empty() { 126 | Ok(None) 127 | } else { 128 | s.parse().map(Some) 129 | } 130 | } 131 | 132 | fn main() { 133 | let args = Args::parse(); 134 | if let Some(max_width) = args.max_width { 135 | if args.min_width > max_width { 136 | eprintln!("error: invalid value '{}' for '--min-width ': must be <= value '{max_width}' for '--max-width '", args.min_width); 137 | std::process::exit(1); 138 | } 139 | } 140 | if let Some(max_height) = args.max_height { 141 | if args.min_height > max_height { 142 | eprintln!("error: invalid value '{}' for '--min-height ': must be <= value '{max_height}' for '--max-height '", args.min_height); 143 | std::process::exit(1); 144 | } 145 | } 146 | 147 | let mut layout_manager = LayoutManager::new( 148 | args.namespace, 149 | LayoutGen::new( 150 | args.min_width, 151 | args.min_height, 152 | args.max_width, 153 | args.max_height, 154 | args.overlap_borders_by, 155 | Weights { 156 | gaps_weight: args.gaps_weight, 157 | overlap_weight: args.overlap_weight, 158 | area_ratios_weight: args.area_ratios_weight, 159 | aspect_ratios_weight: args.aspect_ratios_weight, 160 | adjacent_close_weight: args.adjacent_close_weight, 161 | reading_order_weight: args.reading_order_weight, 162 | center_main_weight: args.center_main_weight, 163 | consistency_weight: args.consistency_weight, 164 | }, 165 | args.area_ratios, 166 | args.aspect_ratios, 167 | ), 168 | ); 169 | 170 | let conn = Connection::connect_to_env().unwrap(); 171 | let mut event_queue = conn.new_event_queue(); 172 | // `get_registry` has necessary side-effects. 173 | let _registry = conn.display().get_registry(&event_queue.handle(), ()); 174 | event_queue.roundtrip(&mut layout_manager).unwrap(); 175 | loop { 176 | event_queue.blocking_dispatch(&mut layout_manager).unwrap(); 177 | } 178 | } 179 | 180 | #[derive(Debug, Clone, Hash, PartialEq, Eq)] 181 | pub struct OutputId(ObjectId); 182 | 183 | impl OutputId { 184 | pub fn new(output: &WlOutput) -> OutputId { 185 | OutputId(output.id()) 186 | } 187 | } 188 | 189 | pub struct LayoutManager { 190 | namespace: String, 191 | gen: LayoutGen, 192 | // These will be initialized 193 | // by Wayland events. 194 | seat: Option>, 195 | manager: Option, 196 | control: Option>>, 197 | } 198 | 199 | impl LayoutManager { 200 | pub fn new(namespace: String, gen: LayoutGen) -> Self { 201 | Self { 202 | namespace, 203 | gen, 204 | seat: None, 205 | manager: None, 206 | control: None, 207 | } 208 | } 209 | } 210 | 211 | impl Dispatch for LayoutManager { 212 | fn event( 213 | state: &mut Self, 214 | registry: &WlRegistry, 215 | event: ::Event, 216 | _: &(), 217 | _: &wayland_client::Connection, 218 | qhandle: &wayland_client::QueueHandle, 219 | ) { 220 | if let wl_registry::Event::Global { 221 | name, 222 | interface, 223 | version, 224 | } = event 225 | { 226 | match interface.as_str() { 227 | "wl_seat" => { 228 | state.seat = Some(Arc::new(registry.bind::( 229 | name, 230 | version, 231 | qhandle, 232 | (), 233 | ))); 234 | } 235 | "wl_output" => { 236 | registry.bind::(name, version, qhandle, ()); 237 | } 238 | "river_layout_manager_v3" => { 239 | state.manager = Some(registry.bind::( 240 | name, 241 | version, 242 | qhandle, 243 | (), 244 | )); 245 | } 246 | "zriver_control_v1" => { 247 | state.control = Some(Arc::new(Mutex::new( 248 | registry.bind::(name, version, qhandle, ()), 249 | ))); 250 | } 251 | _ => {} 252 | } 253 | } 254 | } 255 | } 256 | 257 | impl Dispatch for LayoutManager { 258 | fn event( 259 | _: &mut Self, 260 | _: &WlSeat, 261 | _: ::Event, 262 | _: &(), 263 | _: &wayland_client::Connection, 264 | _: &wayland_client::QueueHandle, 265 | ) { 266 | } 267 | } 268 | 269 | impl Dispatch for LayoutManager { 270 | fn event( 271 | state: &mut Self, 272 | output: &WlOutput, 273 | event: ::Event, 274 | _: &(), 275 | _: &wayland_client::Connection, 276 | qhandle: &wayland_client::QueueHandle, 277 | ) { 278 | if let wl_output::Event::Name { name: _ } = event { 279 | // `get_layout` has necessary side-effects. 280 | state 281 | .manager 282 | .as_ref() 283 | .expect("compositor should support `river_layout_v3`") 284 | .get_layout( 285 | output, 286 | state.namespace.clone(), 287 | qhandle, 288 | OutputId::new(output), 289 | ); 290 | } 291 | } 292 | } 293 | 294 | impl Dispatch for LayoutManager { 295 | fn event( 296 | state: &mut Self, 297 | proxy: &RiverLayoutV3, 298 | event: ::Event, 299 | _output: &OutputId, 300 | conn: &wayland_client::Connection, 301 | qhandle: &wayland_client::QueueHandle, 302 | ) { 303 | match event { 304 | river_layout_v3::Event::LayoutDemand { 305 | view_count, 306 | usable_width, 307 | usable_height, 308 | tags: _, 309 | serial, 310 | } => { 311 | let container = Size::new( 312 | NonZeroUsize::new(usable_width as usize).expect("width should be non-zero"), 313 | NonZeroUsize::new(usable_height as usize).expect("height should be non-zero"), 314 | ); 315 | let view_count = view_count as usize; 316 | 317 | match state.gen.try_layout(container, view_count) { 318 | Status::Finished(layout) => { 319 | for rect in layout { 320 | proxy.push_view_dimensions( 321 | rect.x() as i32, 322 | rect.y() as i32, 323 | rect.width().get() as u32, 324 | rect.height().get() as u32, 325 | serial, 326 | ); 327 | } 328 | proxy.commit("owm".to_owned(), serial); 329 | } 330 | Status::NotStarted => { 331 | let control = Arc::clone( 332 | state 333 | .control 334 | .as_ref() 335 | .expect("River control should be initialized"), 336 | ); 337 | let seat = 338 | Arc::clone(state.seat.as_ref().expect("seat should be initialized")); 339 | let qhandle = qhandle.clone(); 340 | let conn = conn.clone(); 341 | state.gen.layout(container, view_count, move |_| { 342 | // River will send a new layout demand 343 | // if it receives a layout command. 344 | let control = control.lock().unwrap(); 345 | control.add_argument("send-layout-cmd".to_owned()); 346 | control.add_argument("owm".to_owned()); 347 | control.add_argument("retry-layout".to_owned()); 348 | control.run_command(&seat, &qhandle, ()); 349 | let _ = conn.flush(); 350 | }); 351 | } 352 | Status::Started => {} 353 | } 354 | } 355 | river_layout_v3::Event::NamespaceInUse => { 356 | panic!( 357 | "namespace '{}' in use: layout program may already be running", 358 | state.namespace 359 | ); 360 | } 361 | _ => {} 362 | } 363 | } 364 | } 365 | 366 | impl Dispatch for LayoutManager { 367 | fn event( 368 | _: &mut Self, 369 | _: &RiverLayoutManagerV3, 370 | _: ::Event, 371 | _: &(), 372 | _: &wayland_client::Connection, 373 | _: &wayland_client::QueueHandle, 374 | ) { 375 | } 376 | } 377 | 378 | impl Dispatch for LayoutManager { 379 | fn event( 380 | _: &mut Self, 381 | _: &ZriverControlV1, 382 | _: ::Event, 383 | _: &(), 384 | _: &wayland_client::Connection, 385 | _: &wayland_client::QueueHandle, 386 | ) { 387 | } 388 | } 389 | 390 | impl Dispatch for LayoutManager { 391 | fn event( 392 | _: &mut Self, 393 | _: &ZriverCommandCallbackV1, 394 | _: ::Event, 395 | _: &(), 396 | _: &wayland_client::Connection, 397 | _: &wayland_client::QueueHandle, 398 | ) { 399 | } 400 | } 401 | 402 | mod protocol { 403 | // See . 404 | 405 | #![allow(non_upper_case_globals)] 406 | 407 | #[allow(clippy::single_component_path_imports)] // Used by macros. 408 | use wayland_client; 409 | use wayland_client::protocol::*; 410 | 411 | pub mod __layout_interfaces { 412 | use wayland_client::backend as wayland_backend; 413 | use wayland_client::protocol::__interfaces::*; 414 | 415 | wayland_scanner::generate_interfaces!("./protocols/river-layout-v3.xml"); 416 | } 417 | use self::__layout_interfaces::*; 418 | 419 | pub mod __control_interfaces { 420 | use wayland_client::backend as wayland_backend; 421 | use wayland_client::protocol::__interfaces::*; 422 | 423 | wayland_scanner::generate_interfaces!("./protocols/river-control-unstable-v1.xml"); 424 | } 425 | use self::__control_interfaces::*; 426 | 427 | wayland_scanner::generate_client_code!("./protocols/river-layout-v3.xml"); 428 | wayland_scanner::generate_client_code!("./protocols/river-control-unstable-v1.xml"); 429 | } 430 | -------------------------------------------------------------------------------- /src/binary.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use num_traits::{pow, One, Zero}; 4 | use std::ops::{Add, Div, Mul, RangeInclusive, Sub}; 5 | 6 | /// Reduce innermost axis 7 | /// to numbers within range. 8 | /// Leftmost is least significant. 9 | /// 10 | /// # Examples 11 | /// 12 | /// ```ignore 13 | /// // It returns lower bound for empty arrays: 14 | /// assert_eq!(ToFracLe::new(1.0..=2.0, 0)::decode(vec![]), 1.); 15 | /// 16 | /// // It returns lower bound when all bits are false: 17 | /// assert_eq!(ToFracLe::new(0.0..=1.0, 1)::decode(vec![false]), 0.); 18 | /// assert_eq!(ToFracLe::new(1.0..=2.0, 2)::decode(vec![false, false]), 1.); 19 | /// 20 | /// // It returns upper bound when all bits are true: 21 | /// assert_eq!(ToFracLe::new(0.0..=1.0, 1)::decode(vec![true]), 1.); 22 | /// assert_eq!(ToFracLe::new(1.0..=2.0, 2)::decode(vec![true, true]), 2.); 23 | /// 24 | /// // It returns a number between lower and upper bound when some bits are true: 25 | /// assert_eq!(ToFracLe::new(1.0..=4.0, 2)::decode(vec![true, false]), 2.); 26 | /// assert_eq!(ToFracLe::new(1.0..=4.0, 2)::decode(vec![false, true]), 3.); 27 | /// ``` 28 | #[derive(Clone, Debug, PartialEq, Eq)] 29 | pub struct ToFracLE { 30 | to_int: ToIntLE, 31 | start: T, 32 | a: Option, 33 | } 34 | 35 | impl ToFracLE { 36 | pub fn new(range: RangeInclusive, bits_len: usize) -> Self 37 | where 38 | T: Copy + One + Add + Sub + Div, 39 | { 40 | let to_int = ToIntLE::new(); 41 | let (start, end) = range.into_inner(); 42 | Self { 43 | a: if bits_len > 0 { 44 | Some((end - start) / (pow(to_int.two, bits_len) - T::one())) 45 | } else { 46 | None 47 | }, 48 | start, 49 | to_int, 50 | } 51 | } 52 | 53 | pub fn decode(&self, bits: impl IntoIterator) -> T 54 | where 55 | T: Copy + Zero + One + Add + Mul, 56 | { 57 | match self.a { 58 | Some(a) => a * self.to_int.decode(bits) + self.start, 59 | None => self.start, 60 | } 61 | } 62 | } 63 | 64 | /// Reduce to base 10 integer representations of bits. 65 | /// Leftmost is least significant. 66 | /// 67 | /// # Examples 68 | /// 69 | /// ```ignore 70 | /// // It returns 0 when empty: 71 | /// assert_eq!(ToIntLe::new().decode(vec![]), 0_u8); 72 | /// 73 | /// // It returns the base 10 integer represented by binary bits: 74 | /// assert_eq!(ToIntLe::new().decode(vec![false]), 0_u8); 75 | /// assert_eq!(ToIntLe::new().decode(vec![false, false]), 0_u8); 76 | /// assert_eq!(ToIntLe::new().decode(vec![false, false, false]), 0_u8); 77 | /// assert_eq!(ToIntLe::new().decode(vec![true]), 1_u8); 78 | /// assert_eq!(ToIntLe::new().decode(vec![true, true]), 3_u8); 79 | /// assert_eq!(ToIntLe::new().decode(vec![true, true, true]), 7_u8); 80 | /// 81 | /// // It treats leftmost as least significant: 82 | /// assert_eq!(ToIntLe::new().decode(vec![false, true]), 2_u8); 83 | /// assert_eq!(ToIntLe::new().decode(vec![false, false, true]), 4_u8); 84 | /// ``` 85 | #[derive(Clone, Debug, PartialEq, Eq)] 86 | pub struct ToIntLE { 87 | two: T, 88 | } 89 | 90 | impl ToIntLE { 91 | pub fn new() -> Self 92 | where 93 | T: One + Add, 94 | { 95 | Self { 96 | two: T::one() + T::one(), 97 | } 98 | } 99 | 100 | pub fn decode(&self, bits: impl IntoIterator) -> T 101 | where 102 | T: Copy + Zero + One + Add + Mul, 103 | { 104 | bits.into_iter() 105 | .fold((T::zero(), T::one()), |(acc, a), b| { 106 | (if b { acc + a } else { acc }, self.two * a) 107 | }) 108 | .0 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/derive.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_macros)] 2 | #![allow(unused_imports)] 3 | 4 | macro_rules! derive_new_from_bounded_partial_ord { 5 | ( $type:ident < $a:ty : $bound:ident > ) => { 6 | crate::derive::_derive_new_from_bounded_partial_ord!( 7 | $type<$a: $bound>, 8 | $a, 9 | IsIncomparable, 10 | "incomparable" 11 | ); 12 | }; 13 | ( $type:ident {( $inner:ty )} ) => { 14 | crate::derive::_derive_new_from_bounded_partial_ord!( 15 | $type, 16 | $inner, 17 | IsIncomparable, 18 | "incomparable" 19 | ); 20 | }; 21 | } 22 | 23 | macro_rules! derive_new_from_bounded_float { 24 | ( $type:ident < $a:ty : $bound:ident > ) => { 25 | crate::derive::_derive_new_from_bounded_partial_ord!($type<$a: $bound>, $a, IsNan, "NaN"); 26 | }; 27 | ( $type:ident ( $inner:ty ) ) => { 28 | crate::derive::_derive_new_from_bounded_partial_ord!($type, $inner, IsNan, "NaN"); 29 | }; 30 | } 31 | 32 | macro_rules! _derive_new_from_bounded_partial_ord { 33 | ( $type:ident $( < $a:ty : $bound:ident > )?, $inner:ty, $incomparable_name:ident, $incomparable_str:literal ) => { 34 | paste::paste! { 35 | #[doc = "Error returned when '" $type "' is given an invalid value."] 36 | #[derive(Clone, Copy, Debug, thiserror::Error, PartialEq)] 37 | pub enum [] $(< $a : $bound >)? { 38 | #[doc = "Value is " $incomparable_str "."] 39 | #[error("{0} is {}", $incomparable_str)] 40 | $incomparable_name($inner), 41 | /// Value is below lower bound. 42 | #[error("{0} is below lower bound ({})", < $type $(< $a >)? > ::min_value())] 43 | TooLow($inner), 44 | /// Value is above upper bound. 45 | #[error("{0} is above upper bound ({})", < $type $(< $a >)? > ::max_value())] 46 | TooHigh($inner), 47 | } 48 | 49 | impl $(< $a : $bound >)? $type $(< $a >)? { 50 | #[doc = "Return a new '" $type "' if given a valid value."] 51 | pub fn new(value: $inner) -> Result] $(< $a >)? > { 52 | match ( 53 | Self(value).partial_cmp(&Self::min_value()), 54 | Self(value).partial_cmp(&Self::max_value()), 55 | ) { 56 | (None, _) | (_, None) => Err([]::$incomparable_name(value)), 57 | (Some(std::cmp::Ordering::Less), _) => Err([]::TooLow(value)), 58 | (_, Some(std::cmp::Ordering::Greater)) => Err([]::TooHigh(value)), 59 | _ => Ok(Self(value)), 60 | } 61 | } 62 | } 63 | } 64 | }; 65 | } 66 | 67 | macro_rules! derive_new_from_lower_bounded_partial_ord { 68 | ( $type:ident < $a:ty : $bound:ident > ) => { 69 | crate::derive::_derive_new_from_lower_bounded_partial_ord!( 70 | $type<$a: $bound>, 71 | $a, 72 | IsIncomparable, 73 | "incomparable" 74 | ); 75 | }; 76 | ( $type:ident {( $inner:ty )} ) => { 77 | crate::derive::_derive_new_from_lower_bounded_partial_ord!( 78 | $type, 79 | $inner, 80 | IsIncomparable, 81 | "incomparable" 82 | ); 83 | }; 84 | } 85 | 86 | macro_rules! derive_new_from_lower_bounded_float { 87 | ( $type:ident < $a:ty : $bound:ident > ) => { 88 | crate::derive::_derive_new_from_lower_bounded_partial_ord!( 89 | $type<$a: $bound>, 90 | $a, 91 | IsNan, 92 | "NaN" 93 | ); 94 | }; 95 | ( $type:ident ( $inner:ty ) ) => { 96 | crate::derive::_derive_new_from_lower_bounded_partial_ord!($type, $inner, IsNan, "NaN"); 97 | }; 98 | } 99 | 100 | macro_rules! _derive_new_from_lower_bounded_partial_ord { 101 | ( $type:ident $( < $a:ty : $bound:ident > )?, $inner:ty, $incomparable_name:ident, $incomparable_str:literal ) => { 102 | paste::paste! { 103 | #[doc = "Error returned when '" $type "' is given an invalid value."] 104 | #[derive(Clone, Copy, Debug, thiserror::Error, PartialEq)] 105 | pub enum [] $(< $a : $bound >)? { 106 | #[doc = "Value is " $incomparable_str "."] 107 | #[error("{0} is {}", $incomparable_str)] 108 | $incomparable_name($inner), 109 | /// Value is below lower bound. 110 | #[error("{0} is below lower bound ({})", < $type $(< $a >)? > ::min_value())] 111 | TooLow($inner), 112 | } 113 | 114 | impl $(< $a : $bound >)? $type $(< $a >)? { 115 | #[doc = "Return a new '" $type "' if given a valid value."] 116 | pub fn new(value: $inner) -> Result] $(< $a >)? > { 117 | match Self(value).partial_cmp(&Self::min_value()) { 118 | None => Err([]::$incomparable_name(value)), 119 | Some(std::cmp::Ordering::Less) => Err([]::TooLow(value)), 120 | _ => Ok(Self(value)), 121 | } 122 | } 123 | } 124 | } 125 | }; 126 | } 127 | 128 | macro_rules! derive_new_from_lower_bounded { 129 | ( $type:ident ( $inner: ty ) ) => { 130 | paste::paste! { 131 | #[doc = "Error returned when '" $type "' is given a value below the lower bound."] 132 | #[derive(Clone, Copy, Debug, thiserror::Error)] 133 | #[error("{0} is below lower bound ({})", $type::min_value())] 134 | pub struct []($inner); 135 | 136 | impl $type { 137 | #[doc = "Return a new '" $type "' if given a valid value."] 138 | pub fn new(value: $inner) -> Result]> { 139 | if Self(value) < Self::min_value() { 140 | Err([](value)) 141 | } else { 142 | Ok(Self(value)) 143 | } 144 | } 145 | } 146 | } 147 | }; 148 | } 149 | 150 | macro_rules! derive_try_from_from_new { 151 | ( $type:ident ( $inner:ty ) ) => { 152 | paste::paste! { 153 | impl core::convert::TryFrom<$inner> for $type { 154 | type Error = []; 155 | fn try_from(value: $inner) -> Result { 156 | $type::new(value) 157 | } 158 | } 159 | } 160 | }; 161 | } 162 | 163 | macro_rules! derive_from_str_from_try_into { 164 | ( $type:ident ( $inner:ty ) ) => { 165 | paste::paste! { 166 | #[doc = "Error returned when failing to convert from a string or into '" $type "'."] 167 | #[derive(Debug, thiserror::Error)] 168 | pub enum [<$type FromStrError>] { 169 | #[doc = "Error convering to '" $inner "'."] 170 | #[error("{0}")] 171 | FromStr(<$inner as std::str::FromStr>::Err), 172 | #[doc = "Error convering to '" $type "'."] 173 | #[error("{0}")] 174 | TryInto(<$type as TryFrom<$inner>>::Error), 175 | } 176 | 177 | impl std::str::FromStr for $type { 178 | type Err = [<$type FromStrError>]; 179 | 180 | fn from_str(s: &str) -> Result { 181 | s.parse::<$inner>() 182 | .map_err(|e| Self::Err::FromStr(e)) 183 | .and_then(|x| x.try_into().map_err(Self::Err::TryInto)) 184 | } 185 | } 186 | } 187 | }; 188 | } 189 | 190 | macro_rules! derive_into_inner { 191 | ( $type:ident ( $inner:ty ) ) => { 192 | paste::paste! { 193 | impl $type { 194 | #[doc = "Unwrap '" $type "' into inner value."] 195 | pub fn into_inner(self) -> $inner { 196 | self.0 197 | } 198 | } 199 | } 200 | }; 201 | ( $type:ident < $a:ty > ) => { 202 | paste::paste! { 203 | impl < $a > $type < $a > { 204 | #[doc = "Unwrap '" $type "' into inner value."] 205 | pub fn into_inner(self) -> $a { 206 | self.0 207 | } 208 | } 209 | } 210 | }; 211 | } 212 | 213 | pub(crate) use _derive_new_from_bounded_partial_ord; 214 | pub(crate) use _derive_new_from_lower_bounded_partial_ord; 215 | pub(crate) use derive_from_str_from_try_into; 216 | pub(crate) use derive_into_inner; 217 | pub(crate) use derive_new_from_bounded_float; 218 | pub(crate) use derive_new_from_bounded_partial_ord; 219 | pub(crate) use derive_new_from_lower_bounded; 220 | pub(crate) use derive_new_from_lower_bounded_float; 221 | pub(crate) use derive_new_from_lower_bounded_partial_ord; 222 | pub(crate) use derive_try_from_from_new; 223 | -------------------------------------------------------------------------------- /src/encoding.rs: -------------------------------------------------------------------------------- 1 | use std::{num::NonZeroUsize, ops::Range}; 2 | 3 | use ndarray::prelude::*; 4 | 5 | use crate::{ 6 | binary::ToFracLE, 7 | post_processing::{remove_gaps, trim_outside}, 8 | rect::{Rect, Size}, 9 | }; 10 | 11 | #[derive(Clone, Debug)] 12 | pub struct Decoder { 13 | max_size: Size, 14 | container: Size, 15 | count: usize, 16 | x_decoder: ToFracLE, 17 | y_decoder: ToFracLE, 18 | width_decoder: ToFracLE, 19 | height_decoder: ToFracLE, 20 | x_bits_range: Range, 21 | y_bits_range: Range, 22 | width_bits_range: Range, 23 | height_bits_range: Range, 24 | } 25 | 26 | impl Decoder { 27 | pub fn new(min_size: Size, max_size: Size, container: Size, count: usize) -> Self { 28 | debug_assert!(min_size.width <= max_size.width); 29 | debug_assert!(min_size.height <= max_size.height); 30 | debug_assert!(max_size.width <= container.width); 31 | debug_assert!(max_size.height <= container.height); 32 | 33 | let x_max = container.width.get().saturating_sub(min_size.width.get()); 34 | let y_max = container.height.get().saturating_sub(min_size.height.get()); 35 | let width_range = min_size.width.get()..=max_size.width.get(); 36 | let height_range = min_size.height.get()..=max_size.height.get(); 37 | let bits_per_x = reduced_bits_for(x_max); 38 | let bits_per_y = reduced_bits_for(y_max); 39 | let bits_per_width = reduced_bits_for(width_range.end() - width_range.start()); 40 | let bits_per_height = reduced_bits_for(height_range.end() - height_range.start()); 41 | Self { 42 | max_size, 43 | container, 44 | count, 45 | x_decoder: ToFracLE::new(0.0..=(x_max as f64), bits_per_x), 46 | y_decoder: ToFracLE::new(0.0..=(y_max as f64), bits_per_y), 47 | width_decoder: ToFracLE::new( 48 | (*width_range.start() as f64)..=(*width_range.end() as f64), 49 | bits_per_width, 50 | ), 51 | height_decoder: ToFracLE::new( 52 | (*height_range.start() as f64)..=(*height_range.end() as f64), 53 | bits_per_height, 54 | ), 55 | x_bits_range: 0..bits_per_x, 56 | y_bits_range: bits_per_x..(bits_per_x + bits_per_y), 57 | width_bits_range: (bits_per_x + bits_per_y)..(bits_per_x + bits_per_y + bits_per_width), 58 | height_bits_range: (bits_per_x + bits_per_y + bits_per_width) 59 | ..(bits_per_x + bits_per_y + bits_per_width + bits_per_height), 60 | } 61 | } 62 | 63 | pub fn bits(&self) -> usize { 64 | self.bits_per_rect() * self.count 65 | } 66 | 67 | fn bits_per_rect(&self) -> usize { 68 | self.height_bits_range.end 69 | } 70 | 71 | pub fn decode1(&self, bits: ArrayView1) -> Array1 { 72 | Array::from_vec( 73 | self.decode2(bits.into_shape((1, bits.len())).unwrap()) 74 | .into_raw_vec(), 75 | ) 76 | } 77 | 78 | pub fn decode2(&self, bits: ArrayView2) -> Array2 { 79 | let mut rects = bits 80 | .into_shape((bits.nrows(), self.count, self.bits_per_rect())) 81 | .unwrap() 82 | .map_axis(Axis(2), |xs| { 83 | let width = self.width_decoder.decode( 84 | xs.slice(s![self.width_bits_range.clone()]) 85 | .into_iter() 86 | .copied(), 87 | ) as usize; 88 | let height = self.height_decoder.decode( 89 | xs.slice(s![self.height_bits_range.clone()]) 90 | .into_iter() 91 | .copied(), 92 | ) as usize; 93 | Rect::new( 94 | self.x_decoder 95 | .decode(xs.slice(s![self.x_bits_range.clone()]).into_iter().copied()) 96 | as usize, 97 | self.y_decoder 98 | .decode(xs.slice(s![self.y_bits_range.clone()]).into_iter().copied()) 99 | as usize, 100 | // The decoder should ensure these invariants. 101 | unsafe { NonZeroUsize::new_unchecked(width) }, 102 | unsafe { NonZeroUsize::new_unchecked(height) }, 103 | ) 104 | }); 105 | for mut rects in rects.axis_iter_mut(Axis(0)) { 106 | trim_outside(self.container, rects.view_mut()); 107 | remove_gaps(self.max_size, self.container, rects.view_mut()); 108 | } 109 | rects 110 | } 111 | } 112 | 113 | fn reduced_bits_for(x: usize) -> usize { 114 | // 128 was empirically chosen. 115 | // It has no special meaning, 116 | // other than being a power of 2. 117 | bits_for((x as f64 / 128.0).ceil() as usize) 118 | } 119 | 120 | fn bits_for(x: usize) -> usize { 121 | if x == 0 { 122 | 0 123 | } else if x == 1 { 124 | 1 125 | } else { 126 | (x - 1).ilog2() as usize + 1 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod binary; 2 | mod derive; 3 | mod encoding; 4 | mod objective; 5 | mod post_processing; 6 | mod rect; 7 | 8 | #[cfg(test)] 9 | mod testing; 10 | 11 | use std::{ 12 | collections::hash_map::{Entry, HashMap}, 13 | num::NonZeroUsize, 14 | sync::Arc, 15 | thread, 16 | }; 17 | 18 | use encoding::Decoder; 19 | use once_cell::sync::OnceCell; 20 | use optimal::{optimizer::derivative_free::pbil::*, prelude::*}; 21 | use post_processing::overlap_borders; 22 | use rand::prelude::*; 23 | use rand_xoshiro::SplitMix64; 24 | use rayon::prelude::*; 25 | 26 | use crate::objective::Problem; 27 | pub use crate::{ 28 | objective::{AreaRatio, AspectRatio, Weight, Weights}, 29 | rect::{Pos, Rect, Size}, 30 | }; 31 | 32 | #[derive(Debug)] 33 | pub struct LayoutGen { 34 | inner: Arc, 35 | cache: HashMap>>>, 36 | } 37 | 38 | #[derive(Clone, Debug)] 39 | struct RawLayoutGen { 40 | min_width: NonZeroUsize, 41 | min_height: NonZeroUsize, 42 | max_width: Option, 43 | max_height: Option, 44 | overlap_borders_by: usize, 45 | weights: Weights, 46 | area_ratios: Vec, 47 | aspect_ratios: Vec, 48 | } 49 | 50 | type Key = (Size, usize); 51 | 52 | pub enum Status<'a> { 53 | NotStarted, 54 | Started, 55 | Finished(&'a [Rect]), 56 | } 57 | 58 | impl LayoutGen { 59 | #[allow(clippy::too_many_arguments)] 60 | pub fn new( 61 | min_width: NonZeroUsize, 62 | min_height: NonZeroUsize, 63 | max_width: Option, 64 | max_height: Option, 65 | overlap_borders_by: usize, 66 | weights: Weights, 67 | area_ratios: Vec, 68 | aspect_ratios: Vec, 69 | ) -> Self { 70 | Self { 71 | inner: Arc::new(RawLayoutGen { 72 | min_width, 73 | min_height, 74 | max_width, 75 | max_height, 76 | overlap_borders_by, 77 | weights, 78 | area_ratios, 79 | aspect_ratios, 80 | }), 81 | cache: HashMap::new(), 82 | } 83 | } 84 | 85 | pub fn try_layout(&self, container: Size, count: usize) -> Status { 86 | match self.cache.get(&(container, count)) { 87 | Some(cache_cell) => match cache_cell.get() { 88 | Some(layout) => Status::Finished(layout), 89 | None => Status::Started, 90 | }, 91 | None => Status::NotStarted, 92 | } 93 | } 94 | 95 | pub fn layout(&mut self, container: Size, count: usize, callback: F) 96 | where 97 | F: FnOnce(&[Rect]) + Send + 'static, 98 | { 99 | self._layout(container, count, Box::new(callback)) 100 | } 101 | 102 | // `Box` avoids infinite recusion during compilation. 103 | #[allow(clippy::type_complexity)] 104 | fn _layout( 105 | &mut self, 106 | container: Size, 107 | count: usize, 108 | callback: Box, 109 | ) { 110 | let key = (container, count); 111 | if count == 0 { 112 | return (callback)( 113 | self.cache 114 | .entry(key) 115 | .or_insert(Arc::new(OnceCell::new())) 116 | .get_or_init(Vec::new), 117 | ); 118 | } 119 | match self.cache.entry(key) { 120 | Entry::Vacant(entry) => { 121 | let cache_cell = Arc::clone(entry.insert(Arc::new(OnceCell::new()))); 122 | let gen = Arc::clone(&self.inner); 123 | self.layout( 124 | container, 125 | count - 1, 126 | Box::new(move |prev_layout: &[Rect]| { 127 | let prev_layout = prev_layout.to_vec(); 128 | thread::spawn(move || { 129 | let layout = gen.layout(container, prev_layout); 130 | let layout = cache_cell 131 | .try_insert(layout) 132 | .expect("cell should be unset for {key:?}"); 133 | (callback)(layout) 134 | }); 135 | }), 136 | ); 137 | } 138 | Entry::Occupied(entry) => { 139 | let cache_cell = entry.get(); 140 | if let Some(layout) = cache_cell.get() { 141 | (callback)(layout) 142 | } else { 143 | let cache_cell = Arc::clone(cache_cell); 144 | thread::spawn(move || (callback)(cache_cell.wait())); 145 | } 146 | } 147 | } 148 | } 149 | } 150 | 151 | impl RawLayoutGen { 152 | fn layout(&self, container: Size, prev_layout: Vec) -> Vec { 153 | let count = prev_layout.len() + 1; 154 | let max_size = Size::new( 155 | self.max_width 156 | .map_or(container.width, |x| x.min(container.width)), 157 | self.max_height 158 | .map_or(container.height, |x| x.min(container.height)), 159 | ); 160 | let decoder = Decoder::new( 161 | Size::new( 162 | self.min_width.min(container.width), 163 | self.min_height.min(container.height), 164 | ), 165 | max_size, 166 | container, 167 | count, 168 | ); 169 | let problem = Problem::new( 170 | self.weights, 171 | self.area_ratios.clone(), 172 | self.aspect_ratios.clone(), 173 | max_size, 174 | container, 175 | prev_layout, 176 | ); 177 | let mut rects = decoder.decode1( 178 | UntilConvergedConfig { 179 | threshold: ProbabilityThreshold::new(Probability::new(0.9).unwrap()).unwrap(), 180 | } 181 | .argmin( 182 | &mut Config { 183 | num_samples: NumSamples::new( 184 | 500 * std::thread::available_parallelism().map_or(1, |x| x.into()), 185 | ) 186 | .unwrap(), 187 | adjust_rate: AdjustRate::new(0.1).unwrap(), 188 | mutation_chance: MutationChance::new(0.0).unwrap(), 189 | mutation_adjust_rate: MutationAdjustRate::new(0.05).unwrap(), 190 | } 191 | .start_using( 192 | decoder.bits(), 193 | |points| { 194 | (0..points.nrows()) 195 | .into_par_iter() 196 | .map(|i| { 197 | problem.evaluate(decoder.decode1(points.row(i)).as_slice().unwrap()) 198 | }) 199 | .collect::>() 200 | .into() 201 | }, 202 | &mut SplitMix64::seed_from_u64(0), 203 | ), 204 | ) 205 | .view(), 206 | ); 207 | if self.overlap_borders_by > 0 { 208 | overlap_borders(self.overlap_borders_by, container, rects.view_mut()); 209 | } 210 | rects.into_raw_vec() 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/objective/adjacent_close.rs: -------------------------------------------------------------------------------- 1 | use itertools::Itertools; 2 | 3 | use crate::{Pos, Rect, Size}; 4 | 5 | pub struct PlaceAdjacentClose { 6 | worst_case: f64, 7 | } 8 | 9 | impl PlaceAdjacentClose { 10 | pub fn new(container: Size, count: usize) -> Self { 11 | Self { 12 | // This assumes rectangles cannot exceed container bounds. 13 | // `container.width.get()` is not `- 1` 14 | // because we only compare *some* corners. 15 | worst_case: (count.saturating_sub(1) 16 | * (Pos::new(1, 1)) 17 | .dist(Pos::new(container.width.get(), container.height.get() - 1))) 18 | as f64, 19 | } 20 | } 21 | 22 | pub fn evaluate(&self, rects: &[Rect]) -> f64 { 23 | if rects.len() < 2 { 24 | 0.0 25 | } else { 26 | rects 27 | .iter() 28 | .tuple_windows() 29 | .map(|(rect, other)| { 30 | [ 31 | rect.top_left().dist(other.top_right()), 32 | rect.top_left().dist(other.bottom_left()), 33 | rect.top_right().dist(other.top_left()), 34 | rect.top_right().dist(other.bottom_right()), 35 | rect.bottom_left().dist(other.top_left()), 36 | rect.bottom_left().dist(other.bottom_right()), 37 | rect.bottom_right().dist(other.top_right()), 38 | rect.bottom_right().dist(other.bottom_left()), 39 | ] 40 | .into_iter() 41 | .min() 42 | .unwrap() 43 | }) 44 | .sum::() as f64 45 | / self.worst_case 46 | } 47 | } 48 | } 49 | 50 | #[cfg(test)] 51 | mod tests { 52 | use proptest::prelude::*; 53 | use test_strategy::proptest; 54 | 55 | use crate::testing::ContainedRects; 56 | 57 | use super::*; 58 | 59 | #[proptest] 60 | fn place_adjacent_close_returns_values_in_range_0_1(x: ContainedRects) { 61 | prop_assert!((0.0..=1.0) 62 | .contains(&PlaceAdjacentClose::new(x.container, x.rects.len()).evaluate(&x.rects))) 63 | } 64 | 65 | #[test] 66 | fn place_adjacent_close_returns_1_for_worst_case() { 67 | // Worst case is rectangles with min size alternating opposite corners. 68 | let container = Size::new_checked(10, 10); 69 | let rects = [ 70 | Rect::new_checked(0, 0, 1, 1), 71 | Rect::new_checked(9, 9, 1, 1), 72 | Rect::new_checked(0, 0, 1, 1), 73 | ]; 74 | assert_eq!( 75 | PlaceAdjacentClose::new(container, rects.len()).evaluate(&rects), 76 | 1.0 77 | ) 78 | } 79 | 80 | #[test] 81 | fn place_adjacent_close_returns_0_for_best_case() { 82 | let container = Size::new_checked(10, 10); 83 | let rects = [ 84 | Rect::new_checked(0, 0, 5, 5), 85 | Rect::new_checked(0, 5, 5, 5), 86 | Rect::new_checked(5, 5, 5, 5), 87 | ]; 88 | assert_eq!( 89 | PlaceAdjacentClose::new(container, rects.len()).evaluate(&rects), 90 | 0.0 91 | ) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/objective/area_ratios.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | iter::{once, repeat}, 3 | num::NonZeroUsize, 4 | ops::Mul, 5 | }; 6 | 7 | use derive_more::Display; 8 | use itertools::Itertools; 9 | use num_traits::bounds::LowerBounded; 10 | 11 | use crate::{ 12 | derive::{ 13 | derive_from_str_from_try_into, derive_new_from_lower_bounded_float, 14 | derive_try_from_from_new, 15 | }, 16 | Rect, Size, 17 | }; 18 | 19 | pub struct MaintainAreaRatios { 20 | ratios: Vec, 21 | worst_case: f64, 22 | } 23 | 24 | #[derive(Clone, Copy, Debug, Display, PartialEq, PartialOrd)] 25 | pub struct AreaRatio(f64); 26 | 27 | impl LowerBounded for AreaRatio { 28 | fn min_value() -> Self { 29 | Self(1.0) 30 | } 31 | } 32 | 33 | derive_new_from_lower_bounded_float!(AreaRatio(f64)); 34 | derive_try_from_from_new!(AreaRatio(f64)); 35 | derive_from_str_from_try_into!(AreaRatio(f64)); 36 | 37 | impl Mul for AreaRatio { 38 | type Output = f64; 39 | 40 | fn mul(self, rhs: f64) -> Self::Output { 41 | self.0 * rhs 42 | } 43 | } 44 | 45 | impl MaintainAreaRatios { 46 | pub fn new(ratios: Vec, max_size: Size, count: usize) -> Self { 47 | let worst_case = if !ratios.is_empty() && count > 1 { 48 | Self::_evaluate( 49 | ratios 50 | .iter() 51 | .sorted_unstable_by(|x, y| y.partial_cmp(x).unwrap()) 52 | .chain(repeat(ratios.last().unwrap())) 53 | .copied(), 54 | once(unsafe { NonZeroUsize::new_unchecked(1) }) 55 | .chain(repeat(max_size.area())) 56 | .take(count), 57 | ) 58 | } else { 59 | 0.0 60 | }; 61 | Self { ratios, worst_case } 62 | } 63 | 64 | pub fn evaluate(&self, rects: &[Rect]) -> f64 { 65 | if self.worst_case == 0.0 { 66 | 0.0 67 | } else { 68 | Self::_evaluate( 69 | self.ratios 70 | .iter() 71 | .chain(repeat(self.ratios.last().unwrap())) 72 | .copied(), 73 | rects.iter().map(|x| x.area()), 74 | ) / self.worst_case 75 | } 76 | } 77 | 78 | fn _evaluate( 79 | ratios: impl Iterator, 80 | areas: impl Iterator, 81 | ) -> f64 { 82 | areas 83 | .map(|x| x.get() as f64) 84 | .tuple_windows() 85 | .zip(ratios) 86 | // Use `.abs()` instead of `.max(0.0)` 87 | // to encourage later to grow 88 | // when possible. 89 | // Otherwise, 90 | // the last rectangle can always be small 91 | // with no penalty. 92 | .map(|((x, y), ratio)| (ratio * y - x).abs()) 93 | .sum::() 94 | } 95 | } 96 | 97 | #[cfg(test)] 98 | mod tests { 99 | use proptest::prelude::{prop::collection::vec, *}; 100 | use test_strategy::proptest; 101 | 102 | use crate::testing::ContainedRects; 103 | 104 | use super::*; 105 | 106 | #[proptest] 107 | fn maintain_area_ratios_returns_values_in_range_0_1( 108 | #[strategy(vec(1.0..=100.0, 0..=16))] ratios: Vec, 109 | x: ContainedRects, 110 | ) { 111 | prop_assert!((0.0..=1.0).contains( 112 | &MaintainAreaRatios::new( 113 | ratios 114 | .into_iter() 115 | .map(|x| AreaRatio::new(x).unwrap()) 116 | .collect(), 117 | x.container, 118 | x.rects.len() 119 | ) 120 | .evaluate(&x.rects) 121 | )) 122 | } 123 | 124 | #[test] 125 | fn maintain_area_ratios_returns_1_for_worst_case() { 126 | // Note, 127 | // what exactly counts as the worst case 128 | // is uncertain. 129 | // We could define the worst case 130 | // as the reverse of the best case. 131 | // However, 132 | // then the middle rectangle has a good area 133 | // for its position. 134 | let max_size = Size::new_checked(10, 10); 135 | let rects = [ 136 | Rect::new_checked(0, 0, 1, 1), 137 | Rect::new_checked(0, 0, 10, 10), 138 | Rect::new_checked(0, 0, 10, 10), 139 | ]; 140 | assert_eq!( 141 | MaintainAreaRatios::new(vec![AreaRatio(2.0)], max_size, rects.len()).evaluate(&rects), 142 | 1.0 143 | ) 144 | } 145 | 146 | #[test] 147 | fn maintain_area_ratios_returns_0_for_best_case() { 148 | let max_size = Size::new_checked(10, 10); 149 | let rects = [ 150 | Rect::new_checked(0, 0, 10, 10), 151 | Rect::new_checked(0, 0, 10, 5), 152 | Rect::new_checked(0, 0, 5, 5), 153 | ]; 154 | assert_eq!( 155 | MaintainAreaRatios::new(vec![AreaRatio(2.0)], max_size, rects.len()).evaluate(&rects), 156 | 0.0 157 | ) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/objective/aspect_ratios.rs: -------------------------------------------------------------------------------- 1 | use std::iter::repeat; 2 | 3 | use derive_more::Display; 4 | use num_traits::bounds::LowerBounded; 5 | 6 | use crate::{ 7 | derive::{ 8 | derive_from_str_from_try_into, derive_new_from_lower_bounded_float, 9 | derive_try_from_from_new, 10 | }, 11 | Rect, Size, 12 | }; 13 | 14 | pub struct MaintainAspectRatios { 15 | ratios: Vec, 16 | worst_case: f64, 17 | } 18 | 19 | #[derive(Clone, Copy, Debug, Display, PartialEq, PartialOrd)] 20 | pub struct AspectRatio(f64); 21 | 22 | impl LowerBounded for AspectRatio { 23 | fn min_value() -> Self { 24 | Self(f64::EPSILON) 25 | } 26 | } 27 | 28 | derive_new_from_lower_bounded_float!(AspectRatio(f64)); 29 | derive_try_from_from_new!(AspectRatio(f64)); 30 | derive_from_str_from_try_into!(AspectRatio(f64)); 31 | 32 | impl MaintainAspectRatios { 33 | pub fn new(ratios: Vec, max_size: Size, count: usize) -> Self { 34 | // This assumes rectangles cannot have 0 width or height. 35 | let worst_case = if count > 0 && !ratios.is_empty() { 36 | ratios 37 | .iter() 38 | .chain(repeat(ratios.last().unwrap())) 39 | .map(|ratio| { 40 | (abs_ratio(max_size.width.get() as f64 / ratio.0)) 41 | .max(abs_ratio((1.0 / max_size.height.get() as f64) / ratio.0)) 42 | - 1.0 43 | }) 44 | .take(count) 45 | .sum() 46 | } else { 47 | 0.0 48 | }; 49 | Self { ratios, worst_case } 50 | } 51 | 52 | pub fn evaluate(&self, rects: &[Rect]) -> f64 { 53 | if self.worst_case == 0.0 { 54 | 0.0 55 | } else { 56 | rects 57 | .iter() 58 | .zip( 59 | self.ratios 60 | .iter() 61 | .chain(repeat(self.ratios.last().unwrap())) 62 | .copied(), 63 | ) 64 | .map(|(x, ratio)| { 65 | abs_ratio((x.size.width.get() as f64 / x.size.height.get() as f64) / ratio.0) 66 | - 1.0 67 | }) 68 | .sum::() 69 | / self.worst_case 70 | } 71 | } 72 | } 73 | 74 | fn abs_ratio(x: f64) -> f64 { 75 | if x < 1.0 { 76 | 1.0 / x 77 | } else { 78 | x 79 | } 80 | } 81 | 82 | #[cfg(test)] 83 | mod tests { 84 | use proptest::prelude::{prop::collection::vec, *}; 85 | use test_strategy::proptest; 86 | 87 | use crate::testing::ContainedRects; 88 | 89 | use super::*; 90 | 91 | #[proptest] 92 | fn maintain_aspect_ratios_returns_values_in_range_0_1( 93 | #[strategy(vec(f64::EPSILON..=100.0, 0..=16))] ratios: Vec, 94 | x: ContainedRects, 95 | ) { 96 | prop_assert!((0.0..=1.0).contains( 97 | &MaintainAspectRatios::new( 98 | ratios 99 | .into_iter() 100 | .map(|x| AspectRatio::new(x).unwrap()) 101 | .collect(), 102 | x.container, 103 | x.rects.len() 104 | ) 105 | .evaluate(&x.rects) 106 | )) 107 | } 108 | 109 | #[test] 110 | fn maintain_aspect_ratios_returns_1_for_worst_case() { 111 | let max_size = Size::new_checked(10, 10); 112 | let rects = [ 113 | Rect::new_checked(0, 0, 1, 10), 114 | Rect::new_checked(0, 0, 10, 1), 115 | ]; 116 | assert_eq!( 117 | MaintainAspectRatios::new( 118 | vec![AspectRatio(2.0), AspectRatio(0.5)], 119 | max_size, 120 | rects.len() 121 | ) 122 | .evaluate(&rects), 123 | 1.0 124 | ) 125 | } 126 | 127 | #[test] 128 | fn maintain_aspect_ratios_returns_0_for_best_case() { 129 | let max_size = Size::new_checked(10, 10); 130 | let rects = [ 131 | Rect::new_checked(0, 0, 10, 5), 132 | Rect::new_checked(0, 0, 5, 10), 133 | ]; 134 | assert_eq!( 135 | MaintainAspectRatios::new( 136 | vec![AspectRatio(2.0), AspectRatio(0.5)], 137 | max_size, 138 | rects.len() 139 | ) 140 | .evaluate(&rects), 141 | 0.0 142 | ) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/objective/center_main.rs: -------------------------------------------------------------------------------- 1 | use crate::{Pos, Rect, Size}; 2 | 3 | pub struct CenterMain { 4 | center: Pos, 5 | worst_case: f64, 6 | } 7 | 8 | impl CenterMain { 9 | pub fn new(container: Size) -> Self { 10 | let center = Pos::new(container.width.get() / 2, container.height.get() / 2); 11 | Self { 12 | center, 13 | worst_case: center 14 | .dist(Pos::new(0, 0)) 15 | .max(center.dist(container.into())) as f64, 16 | } 17 | } 18 | 19 | pub fn evaluate(&self, rects: &[Rect]) -> f64 { 20 | match rects.get(0) { 21 | Some(rect) => rect.center().dist(self.center) as f64 / self.worst_case, 22 | None => 0.0, 23 | } 24 | } 25 | } 26 | 27 | #[cfg(test)] 28 | mod tests { 29 | use std::iter::once; 30 | 31 | use itertools::Itertools; 32 | use proptest::prelude::*; 33 | use test_strategy::proptest; 34 | 35 | use crate::testing::ContainedRects; 36 | 37 | use super::*; 38 | 39 | #[proptest] 40 | fn center_main_returns_values_in_range_0_1(x: ContainedRects) { 41 | prop_assert!((0.0..=1.0).contains(&CenterMain::new(x.container).evaluate(&x.rects))) 42 | } 43 | 44 | #[test] 45 | fn center_main_returns_1_for_worst_case() { 46 | let container = Size::new_checked(10, 10); 47 | let rects = [ 48 | Rect::new_checked(0, 0, 1, 1), 49 | Rect::new_checked(0, 5, 5, 5), 50 | Rect::new_checked(0, 0, 10, 10), 51 | ]; 52 | assert_eq!(CenterMain::new(container).evaluate(&rects), 1.0) 53 | } 54 | 55 | #[test] 56 | fn center_main_returns_0_for_centered_main() { 57 | let container = Size::new_checked(12, 12); 58 | let rects = [ 59 | Rect::new_checked(3, 3, 6, 6), 60 | Rect::new_checked(0, 0, 12, 12), 61 | Rect::new_checked(0, 5, 5, 5), 62 | ]; 63 | assert_eq!(CenterMain::new(container).evaluate(&rects), 0.0) 64 | } 65 | 66 | #[proptest] 67 | fn center_main_returns_0_for_full_main(x: ContainedRects) { 68 | assert_eq!( 69 | CenterMain::new(x.container).evaluate( 70 | &once(Rect::new(0, 0, x.container.width, x.container.height)) 71 | .chain(x.rects) 72 | .collect_vec() 73 | ), 74 | 0.0 75 | ) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/objective/consistency.rs: -------------------------------------------------------------------------------- 1 | use crate::{Rect, Size}; 2 | 3 | pub struct MaximizeConsistency { 4 | previous_layout: Vec, 5 | worst_case: f64, 6 | } 7 | 8 | impl MaximizeConsistency { 9 | pub fn new(container: Size, previous_layout: Vec) -> Self { 10 | let max_pos_x = container.width.get() - 1; 11 | let max_pos_y = container.height.get() - 1; 12 | let max_width = container.width.get(); 13 | let max_height = container.height.get(); 14 | Self { 15 | // This assumes rects cannot exceed their container. 16 | worst_case: previous_layout 17 | .iter() 18 | .map(|rect| { 19 | [ 20 | Rect::new_checked(0, 0, 1, 1), 21 | Rect::new_checked(0, 0, max_width, 1), 22 | Rect::new_checked(0, 0, 1, max_height), 23 | Rect::new_checked(0, 0, max_width, max_height), 24 | Rect::new_checked(max_pos_x, 0, 1, 1), 25 | Rect::new_checked(max_pos_x, 0, 1, max_height), 26 | Rect::new_checked(0, max_pos_y, 1, 1), 27 | Rect::new_checked(0, max_pos_y, max_width, 1), 28 | Rect::new_checked(max_pos_x, max_pos_y, 1, 1), 29 | ] 30 | .into_iter() 31 | .map(|other| rect.diff(other)) 32 | .max() 33 | .unwrap() 34 | }) 35 | .sum::() as f64, 36 | previous_layout, 37 | } 38 | } 39 | 40 | pub fn evaluate(&self, rects: &[Rect]) -> f64 { 41 | if self.worst_case == 0.0 { 42 | 0.0 43 | } else { 44 | rects 45 | .iter() 46 | .zip(&self.previous_layout) 47 | .map(|(rect, prev)| rect.diff(*prev)) 48 | .sum::() as f64 49 | / self.worst_case 50 | } 51 | } 52 | } 53 | 54 | #[cfg(test)] 55 | mod tests { 56 | use itertools::Itertools; 57 | use proptest::prelude::*; 58 | use test_strategy::proptest; 59 | 60 | use crate::testing::{ContainedRects, ContainedRectsParams}; 61 | 62 | use super::*; 63 | 64 | #[proptest] 65 | fn maximize_consistency_returns_values_in_range_0_1( 66 | #[strategy(arbitrary_maximize_consistency_args())] args: (ContainedRects, Vec), 67 | ) { 68 | prop_assert!((0.0..=1.0) 69 | .contains(&MaximizeConsistency::new(args.0.container, args.0.rects).evaluate(&args.1))) 70 | } 71 | 72 | #[test] 73 | fn maximize_consistency_returns_1_for_worst_case() { 74 | let container = Size::new_checked(10, 10); 75 | let prev = [ 76 | Rect::new_checked(0, 0, 10, 10), 77 | Rect::new_checked(9, 9, 1, 1), 78 | Rect::new_checked(4, 4, 1, 1), 79 | ]; 80 | let rects = [ 81 | Rect::new_checked(9, 9, 1, 1), 82 | Rect::new_checked(0, 0, 10, 10), 83 | Rect::new_checked(0, 0, 10, 10), 84 | Rect::new_checked(0, 0, 1, 1), 85 | ]; 86 | assert_eq!( 87 | MaximizeConsistency::new(container, prev.to_vec()).evaluate(&rects), 88 | 1.0 89 | ); 90 | } 91 | 92 | #[proptest] 93 | fn maximize_consistency_returns_0_for_best_case( 94 | #[strategy(arbitrary_maximize_consistency_args())] args: (ContainedRects, Vec), 95 | ) { 96 | assert_eq!( 97 | MaximizeConsistency::new(args.0.container, args.0.rects.clone()).evaluate( 98 | &args 99 | .0 100 | .rects 101 | .into_iter() 102 | .chain(args.1.into_iter()) 103 | .collect_vec() 104 | ), 105 | 0.0 106 | ); 107 | } 108 | 109 | fn arbitrary_maximize_consistency_args() -> BoxedStrategy<(ContainedRects, Vec)> { 110 | ContainedRects::arbitrary() 111 | .prop_flat_map(|x| { 112 | ( 113 | ContainedRects::arbitrary_with(ContainedRectsParams { 114 | width_range: x.container.width..=x.container.width, 115 | height_range: x.container.height..=x.container.height, 116 | len_range: (x.rects.len() + 1)..=(x.rects.len() + 17), 117 | }), 118 | Just(x), 119 | ) 120 | }) 121 | .prop_map(|(x, y)| (y, x.rects)) 122 | .boxed() 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/objective/gaps.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroUsize; 2 | 3 | use crate::{rect::covered_area, Rect, Size}; 4 | 5 | pub struct MinimizeGaps { 6 | area: NonZeroUsize, 7 | worst_case: f64, 8 | } 9 | 10 | impl MinimizeGaps { 11 | pub fn new(container: Size) -> Self { 12 | Self { 13 | area: container.area(), 14 | worst_case: (container.area().get() - 1) as f64, 15 | } 16 | } 17 | 18 | pub fn evaluate(&self, rects: &[Rect]) -> f64 { 19 | if rects.is_empty() { 20 | 1.0 21 | } else { 22 | // This assumes rectangles do not exceed container bounds. 23 | // Worst case can theoretically be zero, 24 | // if `container.area()` is `1`, 25 | // but this is unrealistic in practice. 26 | (self.area.get() - covered_area(rects).get()) as f64 / self.worst_case 27 | } 28 | } 29 | } 30 | 31 | #[cfg(test)] 32 | mod tests { 33 | use std::iter::{once, repeat}; 34 | 35 | use itertools::Itertools; 36 | use proptest::prelude::*; 37 | use test_strategy::proptest; 38 | 39 | use crate::testing::ContainedRects; 40 | 41 | use super::*; 42 | 43 | #[proptest] 44 | fn minimize_gaps_returns_values_in_range_0_1(x: ContainedRects) { 45 | prop_assert!((0.0..=1.0).contains(&MinimizeGaps::new(x.container).evaluate(&x.rects))) 46 | } 47 | 48 | #[proptest] 49 | fn minimize_gaps_returns_1_for_worst_case( 50 | container: Size, 51 | #[strategy((0_usize..=16))] count: usize, 52 | ) { 53 | prop_assume!(container.width.get() > 1 || container.height.get() > 1); 54 | prop_assert_eq!( 55 | MinimizeGaps::new(container).evaluate( 56 | &repeat(Rect::new_checked(0, 0, 1, 1)) 57 | .take(count) 58 | .collect_vec() 59 | ), 60 | 1.0 61 | ) 62 | } 63 | 64 | #[test] 65 | fn minimize_gaps_returns_0_for_best_case_without_overlap() { 66 | let container = Size::new_checked(10, 10); 67 | let rects = [ 68 | Rect::new_checked(0, 0, 10, 5), 69 | Rect::new_checked(0, 5, 5, 5), 70 | Rect::new_checked(5, 5, 5, 5), 71 | ]; 72 | assert_eq!(MinimizeGaps::new(container).evaluate(&rects), 0.0) 73 | } 74 | 75 | #[proptest] 76 | fn minimize_gaps_returns_0_for_best_case_with_overlap(x: ContainedRects) { 77 | prop_assert_eq!( 78 | MinimizeGaps::new(x.container).evaluate( 79 | &once(Rect::new(0, 0, x.container.width, x.container.height)) 80 | .chain(x.rects) 81 | .collect_vec() 82 | ), 83 | 0.0 84 | ) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/objective/mod.rs: -------------------------------------------------------------------------------- 1 | mod adjacent_close; 2 | mod area_ratios; 3 | mod aspect_ratios; 4 | mod center_main; 5 | mod consistency; 6 | mod gaps; 7 | mod overlap; 8 | mod reading_order; 9 | 10 | use std::ops::Mul; 11 | 12 | use derive_more::Display; 13 | use num_traits::bounds::LowerBounded; 14 | 15 | use crate::{ 16 | derive::*, 17 | rect::{Rect, Size}, 18 | }; 19 | 20 | use self::{ 21 | adjacent_close::PlaceAdjacentClose, area_ratios::MaintainAreaRatios, 22 | aspect_ratios::MaintainAspectRatios, center_main::CenterMain, consistency::MaximizeConsistency, 23 | gaps::MinimizeGaps, overlap::MinimizeOverlap, reading_order::PlaceInReadingOrder, 24 | }; 25 | pub use self::{area_ratios::AreaRatio, aspect_ratios::AspectRatio}; 26 | 27 | pub struct Problem { 28 | weights: Weights, 29 | gaps: MinimizeGaps, 30 | overlap: MinimizeOverlap, 31 | area_ratios: MaintainAreaRatios, 32 | aspect_ratios: MaintainAspectRatios, 33 | adjacent_close: PlaceAdjacentClose, 34 | reading_order: PlaceInReadingOrder, 35 | center_main: CenterMain, 36 | consistency: MaximizeConsistency, 37 | } 38 | 39 | #[derive(Clone, Copy, Debug)] 40 | pub struct Weights { 41 | pub gaps_weight: Weight, 42 | pub overlap_weight: Weight, 43 | pub area_ratios_weight: Weight, 44 | pub aspect_ratios_weight: Weight, 45 | pub adjacent_close_weight: Weight, 46 | pub reading_order_weight: Weight, 47 | pub center_main_weight: Weight, 48 | pub consistency_weight: Weight, 49 | } 50 | 51 | #[derive(Clone, Copy, Debug, Display, PartialEq, PartialOrd)] 52 | pub struct Weight(f64); 53 | 54 | impl LowerBounded for Weight { 55 | fn min_value() -> Self { 56 | Self(0.0) 57 | } 58 | } 59 | 60 | derive_new_from_lower_bounded_float!(Weight(f64)); 61 | derive_try_from_from_new!(Weight(f64)); 62 | derive_from_str_from_try_into!(Weight(f64)); 63 | 64 | impl Mul for Weight { 65 | type Output = f64; 66 | 67 | fn mul(self, rhs: f64) -> Self::Output { 68 | self.0 * rhs 69 | } 70 | } 71 | 72 | impl Problem { 73 | pub fn new( 74 | weights: Weights, 75 | area_ratios: Vec, 76 | aspect_ratios: Vec, 77 | max_size: Size, 78 | container: Size, 79 | prev_layout: Vec, 80 | ) -> Self { 81 | let count = prev_layout.len() + 1; 82 | Self { 83 | weights, 84 | gaps: MinimizeGaps::new(container), 85 | overlap: MinimizeOverlap::new(container, count), 86 | area_ratios: MaintainAreaRatios::new(area_ratios, max_size, count), 87 | aspect_ratios: MaintainAspectRatios::new(aspect_ratios, max_size, count), 88 | adjacent_close: PlaceAdjacentClose::new(container, count), 89 | reading_order: PlaceInReadingOrder::new(count), 90 | center_main: CenterMain::new(container), 91 | consistency: MaximizeConsistency::new(container, prev_layout), 92 | } 93 | } 94 | 95 | pub fn evaluate(&self, rects: &[Rect]) -> f64 { 96 | (if self.weights.gaps_weight > Weight(0.0) { 97 | self.weights.gaps_weight * self.gaps.evaluate(rects) 98 | } else { 99 | 0.0 100 | }) + (if self.weights.overlap_weight > Weight(0.0) { 101 | self.weights.overlap_weight * self.overlap.evaluate(rects) 102 | } else { 103 | 0.0 104 | }) + (if self.weights.area_ratios_weight > Weight(0.0) { 105 | self.weights.area_ratios_weight * self.area_ratios.evaluate(rects) 106 | } else { 107 | 0.0 108 | }) + (if self.weights.aspect_ratios_weight > Weight(0.0) { 109 | self.weights.aspect_ratios_weight * self.aspect_ratios.evaluate(rects) 110 | } else { 111 | 0.0 112 | }) + (if self.weights.adjacent_close_weight > Weight(0.0) { 113 | self.weights.adjacent_close_weight * self.adjacent_close.evaluate(rects) 114 | } else { 115 | 0.0 116 | }) + (if self.weights.reading_order_weight > Weight(0.0) { 117 | self.weights.reading_order_weight * self.reading_order.evaluate(rects) 118 | } else { 119 | 0.0 120 | }) + (if self.weights.center_main_weight > Weight(0.0) { 121 | self.weights.center_main_weight * self.center_main.evaluate(rects) 122 | } else { 123 | 0.0 124 | }) + (if self.weights.consistency_weight > Weight(0.0) { 125 | self.weights.consistency_weight * self.consistency.evaluate(rects) 126 | } else { 127 | 0.0 128 | }) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/objective/overlap.rs: -------------------------------------------------------------------------------- 1 | use crate::{rect::obscured_area, Rect, Size}; 2 | 3 | pub struct MinimizeOverlap { 4 | worst_case: f64, 5 | } 6 | 7 | impl MinimizeOverlap { 8 | pub fn new(container: Size, count: usize) -> Self { 9 | Self { 10 | worst_case: (count.saturating_sub(1) * container.area().get()) as f64, 11 | } 12 | } 13 | 14 | pub fn evaluate(&self, rects: &[Rect]) -> f64 { 15 | if rects.len() < 2 { 16 | 0.0 17 | } else { 18 | obscured_area(rects) as f64 / self.worst_case 19 | } 20 | } 21 | } 22 | #[cfg(test)] 23 | mod tests { 24 | use std::iter::repeat; 25 | 26 | use itertools::Itertools; 27 | use proptest::prelude::*; 28 | use test_strategy::proptest; 29 | 30 | use crate::testing::ContainedRects; 31 | 32 | use super::*; 33 | 34 | #[proptest] 35 | fn minimize_overlap_returns_values_in_range_0_1(x: ContainedRects) { 36 | prop_assert!((0.0..=1.0) 37 | .contains(&MinimizeOverlap::new(x.container, x.rects.len()).evaluate(&x.rects))) 38 | } 39 | 40 | #[proptest] 41 | fn minimize_overlap_returns_1_for_worst_case( 42 | container: Size, 43 | #[strategy((2_usize..=16))] count: usize, 44 | ) { 45 | prop_assert_eq!( 46 | MinimizeOverlap::new(container, count).evaluate( 47 | &repeat(Rect::new(0, 0, container.width, container.height)) 48 | .take(count) 49 | .collect_vec() 50 | ), 51 | 1.0 52 | ) 53 | } 54 | 55 | #[proptest] 56 | fn minimize_overlap_returns_0_for_less_than_2_rects( 57 | container: Size, 58 | #[strategy((0_usize..=1))] count: usize, 59 | ) { 60 | prop_assert_eq!( 61 | MinimizeOverlap::new(container, count).evaluate( 62 | &repeat(Rect::new(0, 0, container.width, container.height)) 63 | .take(count) 64 | .collect_vec() 65 | ), 66 | 0.0 67 | ) 68 | } 69 | 70 | #[test] 71 | fn minimize_overlap_returns_0_for_best_case() { 72 | let container = Size::new_checked(10, 10); 73 | let rects = [ 74 | Rect::new_checked(0, 0, 10, 5), 75 | Rect::new_checked(0, 5, 5, 5), 76 | Rect::new_checked(5, 5, 5, 5), 77 | ]; 78 | assert_eq!( 79 | MinimizeOverlap::new(container, rects.len()).evaluate(&rects), 80 | 0.0 81 | ) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/objective/reading_order.rs: -------------------------------------------------------------------------------- 1 | use itertools::Itertools; 2 | 3 | use crate::Rect; 4 | 5 | pub struct PlaceInReadingOrder { 6 | worst_case: f64, 7 | } 8 | 9 | impl PlaceInReadingOrder { 10 | pub fn new(count: usize) -> Self { 11 | Self { 12 | worst_case: count.saturating_sub(1) as f64, 13 | } 14 | } 15 | 16 | pub fn evaluate(&self, rects: &[Rect]) -> f64 { 17 | if rects.len() < 2 { 18 | 0.0 19 | } else { 20 | rects 21 | .iter() 22 | .tuple_windows() 23 | .filter(|(rect, other)| other.top() < rect.top() || other.left() < rect.left()) 24 | .count() as f64 25 | / self.worst_case 26 | } 27 | } 28 | } 29 | 30 | #[cfg(test)] 31 | mod tests { 32 | use proptest::prelude::*; 33 | use test_strategy::proptest; 34 | 35 | use crate::testing::ContainedRects; 36 | 37 | use super::*; 38 | 39 | #[proptest] 40 | fn place_in_reading_order_returns_values_in_range_0_1(x: ContainedRects) { 41 | prop_assert!( 42 | (0.0..=1.0).contains(&PlaceInReadingOrder::new(x.rects.len()).evaluate(&x.rects)) 43 | ) 44 | } 45 | 46 | #[test] 47 | fn place_in_reading_order_returns_1_for_worst_case() { 48 | let rects = [ 49 | Rect::new_checked(2, 0, 1, 1), 50 | Rect::new_checked(1, 0, 1, 1), 51 | Rect::new_checked(0, 0, 1, 1), 52 | ]; 53 | assert_eq!(PlaceInReadingOrder::new(rects.len()).evaluate(&rects), 1.0); 54 | let rects = [ 55 | Rect::new_checked(0, 2, 1, 1), 56 | Rect::new_checked(0, 1, 1, 1), 57 | Rect::new_checked(0, 0, 1, 1), 58 | ]; 59 | assert_eq!(PlaceInReadingOrder::new(rects.len()).evaluate(&rects), 1.0); 60 | } 61 | 62 | #[test] 63 | fn place_in_reading_order_returns_0_for_best_case() { 64 | let rects = [ 65 | Rect::new_checked(0, 0, 1, 1), 66 | Rect::new_checked(1, 0, 1, 1), 67 | Rect::new_checked(2, 0, 1, 1), 68 | ]; 69 | assert_eq!(PlaceInReadingOrder::new(rects.len()).evaluate(&rects), 0.0); 70 | let rects = [ 71 | Rect::new_checked(0, 0, 1, 1), 72 | Rect::new_checked(0, 1, 1, 1), 73 | Rect::new_checked(0, 2, 1, 1), 74 | ]; 75 | assert_eq!(PlaceInReadingOrder::new(rects.len()).evaluate(&rects), 0.0); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/post_processing.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroUsize; 2 | 3 | use itertools::Itertools; 4 | use ndarray::prelude::*; 5 | 6 | use crate::rect::{RangeExclusive, Rect, Size}; 7 | 8 | pub fn trim_outside(container: Size, mut rects: ArrayViewMut1) { 9 | for rect in rects.iter_mut() { 10 | rect.size.width = 11 | NonZeroUsize::new(rect.width().get().min(container.width.get() - rect.x())) 12 | .unwrap_or(unsafe { NonZeroUsize::new_unchecked(1) }); 13 | rect.size.height = 14 | NonZeroUsize::new(rect.height().get().min(container.height.get() - rect.y())) 15 | .unwrap_or(unsafe { NonZeroUsize::new_unchecked(1) }); 16 | } 17 | } 18 | 19 | pub fn remove_gaps(max_size: Size, container: Size, mut rects: ArrayViewMut1) { 20 | debug_assert!(max_size.width <= container.width); 21 | debug_assert!(max_size.height <= container.height); 22 | 23 | let flip_flop = |dist, x: usize, y: usize| { 24 | let x_ = x.min(div_ceil(dist, 2)); 25 | let y = y.min(dist - x_); 26 | let x = x.min(dist - y); 27 | (x, y) 28 | }; 29 | 30 | let mut freedoms = rects 31 | .iter() 32 | .map(|rect| Freedoms { 33 | left: rect.left(), 34 | right: container.width.get().saturating_sub(rect.right()), 35 | top: rect.top(), 36 | bottom: container.height.get().saturating_sub(rect.bottom()), 37 | }) 38 | .collect_vec(); 39 | loop { 40 | // These bounds may overestimate. 41 | // However, 42 | // for the algorithm to work 43 | // they only need to not underestimate, 44 | // as long as they are accurate 45 | // when freedom is zero. 46 | let x_rays = rects 47 | .iter() 48 | .zip(freedoms.iter()) 49 | .map(|(rect, freedoms)| { 50 | let y_range = rect.y_range_exclusive(); 51 | let max_free = max_size.width.get().saturating_sub(rect.width().get()); 52 | let left = if freedoms.left == 0 { 53 | rect.left() 54 | } else { 55 | rects 56 | .iter() 57 | .filter(|other| { 58 | other.right() < rect.left() 59 | && y_range.intersects(other.y_range_exclusive()) 60 | }) 61 | .map(|other| other.right()) 62 | .max() 63 | .unwrap_or(0) 64 | .max(rect.left().saturating_sub(max_free)) 65 | }; 66 | let right = if freedoms.right == 0 { 67 | rect.right() 68 | } else { 69 | rects 70 | .iter() 71 | .filter(|other| { 72 | rect.right() < other.left() 73 | && y_range.intersects(other.y_range_exclusive()) 74 | }) 75 | .map(|other| other.left()) 76 | .min() 77 | .unwrap_or(container.width.get()) 78 | .min(rect.right() + max_free) 79 | }; 80 | RangeExclusive(left, right) 81 | }) 82 | .collect_vec(); 83 | let y_rays = rects 84 | .iter() 85 | .zip(freedoms.iter()) 86 | .map(|(rect, freedoms)| { 87 | let x_range = rect.x_range_exclusive(); 88 | let max_free = max_size.height.get().saturating_sub(rect.height().get()); 89 | let top = if freedoms.top == 0 { 90 | rect.top() 91 | } else { 92 | rects 93 | .iter() 94 | .filter(|other| { 95 | other.bottom() < rect.top() 96 | && x_range.intersects(other.x_range_exclusive()) 97 | }) 98 | .map(|other| other.bottom()) 99 | .max() 100 | .unwrap_or(0) 101 | .max(rect.top().saturating_sub(max_free)) 102 | }; 103 | let bottom = if freedoms.bottom == 0 { 104 | rect.bottom() 105 | } else { 106 | rects 107 | .iter() 108 | .filter(|other| { 109 | rect.bottom() < other.top() 110 | && x_range.intersects(other.x_range_exclusive()) 111 | }) 112 | .map(|other| other.top()) 113 | .min() 114 | .unwrap_or(container.height.get()) 115 | .min(rect.bottom() + max_free) 116 | }; 117 | RangeExclusive(top, bottom) 118 | }) 119 | .collect_vec(); 120 | 121 | for (rect, freedoms) in rects.iter().zip(freedoms.iter_mut()) { 122 | let (left, right) = flip_flop( 123 | max_size.width.get().saturating_sub(rect.width().get()), 124 | rect.left(), 125 | container.width.get().saturating_sub(rect.right()), 126 | ); 127 | freedoms.left = left; 128 | freedoms.right = right; 129 | let (top, bottom) = flip_flop( 130 | max_size.height.get().saturating_sub(rect.height().get()), 131 | rect.top(), 132 | container.height.get().saturating_sub(rect.bottom()), 133 | ); 134 | freedoms.top = top; 135 | freedoms.bottom = bottom; 136 | } 137 | for (((i, rect), (x_ray, y_ray)), ((other_i, other_rect), (other_x_ray, other_y_ray))) in 138 | rects 139 | .iter() 140 | .enumerate() 141 | .zip(x_rays.iter().zip(y_rays.iter())) 142 | .tuple_combinations() 143 | { 144 | let x_range = rect.x_range_exclusive(); 145 | let other_x_range = other_rect.x_range_exclusive(); 146 | let y_range = rect.y_range_exclusive(); 147 | let other_y_range = other_rect.y_range_exclusive(); 148 | 149 | if y_ray.intersects(*other_y_ray) { 150 | let y_intersects = y_range.intersects(other_y_range); 151 | if y_intersects { 152 | if other_x_range.contains(rect.left()) { 153 | freedoms.get_mut(i).unwrap().left = 0; 154 | } 155 | if x_range.contains(other_rect.right()) { 156 | freedoms.get_mut(other_i).unwrap().right = 0; 157 | } 158 | if other_x_range.contains(rect.right()) { 159 | freedoms.get_mut(i).unwrap().right = 0; 160 | } 161 | if x_range.contains(other_rect.left()) { 162 | freedoms.get_mut(other_i).unwrap().left = 0; 163 | } 164 | } 165 | if other_rect.right() <= rect.left() { 166 | let dist = rect.left() - other_rect.right(); 167 | if dist > 0 || y_intersects { 168 | let (left, right) = 169 | flip_flop(dist, freedoms[i].left, freedoms[other_i].right); 170 | freedoms.get_mut(i).unwrap().left = left; 171 | freedoms.get_mut(other_i).unwrap().right = right; 172 | } 173 | } 174 | if rect.right() <= other_rect.left() { 175 | let dist = other_rect.left() - rect.right(); 176 | if dist > 0 || y_intersects { 177 | let (right, left) = 178 | flip_flop(dist, freedoms[i].right, freedoms[other_i].left); 179 | freedoms.get_mut(i).unwrap().right = right; 180 | freedoms.get_mut(other_i).unwrap().left = left; 181 | } 182 | } 183 | } 184 | 185 | if x_ray.intersects(*other_x_ray) { 186 | let x_intersects = x_range.intersects(other_x_range); 187 | if x_intersects { 188 | if other_y_range.contains(rect.top()) { 189 | freedoms.get_mut(i).unwrap().top = 0; 190 | } 191 | if y_range.contains(other_rect.bottom()) { 192 | freedoms.get_mut(other_i).unwrap().bottom = 0; 193 | } 194 | if other_y_range.contains(rect.bottom()) { 195 | freedoms.get_mut(i).unwrap().bottom = 0; 196 | } 197 | if y_range.contains(other_rect.top()) { 198 | freedoms.get_mut(other_i).unwrap().top = 0; 199 | } 200 | } 201 | if other_rect.bottom() <= rect.top() { 202 | let dist = rect.top() - other_rect.bottom(); 203 | if dist > 0 || x_intersects { 204 | let (top, bottom) = 205 | flip_flop(dist, freedoms[i].top, freedoms[other_i].bottom); 206 | freedoms.get_mut(i).unwrap().top = top; 207 | freedoms.get_mut(other_i).unwrap().bottom = bottom; 208 | } 209 | } 210 | if rect.bottom() <= other_rect.top() { 211 | let dist = other_rect.top() - rect.bottom(); 212 | if dist > 0 || x_intersects { 213 | let (bottom, top) = 214 | flip_flop(dist, freedoms[i].bottom, freedoms[other_i].top); 215 | freedoms.get_mut(i).unwrap().bottom = bottom; 216 | freedoms.get_mut(other_i).unwrap().top = top; 217 | } 218 | } 219 | } 220 | } 221 | 222 | let largest_safe_step = freedoms 223 | .iter() 224 | .flat_map(|freedoms| freedoms.flatten()) 225 | .filter(|x| x > &0) 226 | .min(); 227 | match largest_safe_step { 228 | Some(largest_safe_step) => { 229 | for (rect, freedoms) in rects.iter_mut().zip(freedoms.iter()) { 230 | rect.expand_left(freedoms.left.min(largest_safe_step)); 231 | rect.expand_right(freedoms.right.min(largest_safe_step)); 232 | rect.expand_top(freedoms.top.min(largest_safe_step)); 233 | rect.expand_bottom(freedoms.bottom.min(largest_safe_step)); 234 | } 235 | } 236 | None => break, 237 | } 238 | } 239 | } 240 | 241 | pub fn overlap_borders(border_thickness: usize, container: Size, mut rects: ArrayViewMut1) { 242 | let border_thickness_half_ceil = div_ceil(border_thickness, 2); 243 | let border_thickness_half = border_thickness / 2; 244 | 245 | let filter_map = |i, 246 | other_i, 247 | range: RangeExclusive, 248 | other_range: RangeExclusive, 249 | left, 250 | right| { 251 | if i != other_i && range.intersects(other_range) && left >= right { 252 | Some((left - right, other_i)) 253 | } else { 254 | None 255 | } 256 | }; 257 | 258 | let filter_out_of_range = |(x, i)| { 259 | if x <= border_thickness { 260 | Some(i) 261 | } else { 262 | None 263 | } 264 | }; 265 | 266 | let borders = rects 267 | .iter() 268 | .enumerate() 269 | .map(|(i, rect)| { 270 | let x_range = rect.x_range_exclusive(); 271 | let y_range = rect.y_range_exclusive(); 272 | Sides { 273 | left: { 274 | rects 275 | .iter() 276 | .enumerate() 277 | .filter_map(|(other_i, other_rect)| { 278 | filter_map( 279 | i, 280 | other_i, 281 | y_range, 282 | other_rect.y_range_exclusive(), 283 | rect.left(), 284 | other_rect.right(), 285 | ) 286 | }) 287 | .min() 288 | .and_then(filter_out_of_range) 289 | }, 290 | right: { 291 | rects 292 | .iter() 293 | .enumerate() 294 | .filter_map(|(other_i, other_rect)| { 295 | filter_map( 296 | i, 297 | other_i, 298 | y_range, 299 | other_rect.y_range_exclusive(), 300 | other_rect.left(), 301 | rect.right(), 302 | ) 303 | }) 304 | .min() 305 | .and_then(filter_out_of_range) 306 | }, 307 | top: { 308 | rects 309 | .iter() 310 | .enumerate() 311 | .filter_map(|(other_i, other_rect)| { 312 | filter_map( 313 | i, 314 | other_i, 315 | x_range, 316 | other_rect.x_range_exclusive(), 317 | rect.top(), 318 | other_rect.bottom(), 319 | ) 320 | }) 321 | .min() 322 | .and_then(filter_out_of_range) 323 | }, 324 | bottom: { 325 | rects 326 | .iter() 327 | .enumerate() 328 | .filter_map(|(other_i, other_rect)| { 329 | filter_map( 330 | i, 331 | other_i, 332 | x_range, 333 | other_rect.x_range_exclusive(), 334 | other_rect.top(), 335 | rect.bottom(), 336 | ) 337 | }) 338 | .min() 339 | .and_then(filter_out_of_range) 340 | }, 341 | } 342 | }) 343 | .collect_vec(); 344 | 345 | for ((i, rect), borders) in rects.iter_mut().enumerate().zip(borders.iter()) { 346 | match borders.left { 347 | Some(other_i) => { 348 | if i < other_i { 349 | rect.expand_left(border_thickness_half_ceil); 350 | } else { 351 | rect.expand_left(border_thickness_half); 352 | } 353 | } 354 | None => { 355 | rect.expand_left(border_thickness.min(rect.left())); 356 | } 357 | } 358 | match borders.right { 359 | Some(other_i) => { 360 | if i < other_i { 361 | rect.expand_right(border_thickness_half_ceil); 362 | } else { 363 | rect.expand_right(border_thickness_half); 364 | } 365 | } 366 | None => { 367 | rect.expand_right( 368 | border_thickness.min(container.width.get().saturating_sub(rect.right())), 369 | ); 370 | } 371 | } 372 | match borders.top { 373 | Some(other_i) => { 374 | if i < other_i { 375 | rect.expand_top(border_thickness_half_ceil); 376 | } else { 377 | rect.expand_top(border_thickness_half); 378 | } 379 | } 380 | None => { 381 | rect.expand_top(border_thickness.min(rect.top())); 382 | } 383 | } 384 | match borders.bottom { 385 | Some(other_i) => { 386 | if i < other_i { 387 | rect.expand_bottom(border_thickness_half_ceil); 388 | } else { 389 | rect.expand_bottom(border_thickness_half); 390 | } 391 | } 392 | None => { 393 | rect.expand_bottom( 394 | border_thickness.min(container.height.get().saturating_sub(rect.bottom())), 395 | ); 396 | } 397 | } 398 | } 399 | } 400 | 401 | type Freedoms = Sides; 402 | 403 | #[derive(Clone, Copy, Debug)] 404 | struct Sides { 405 | left: T, 406 | right: T, 407 | top: T, 408 | bottom: T, 409 | } 410 | 411 | impl Sides { 412 | fn flatten(self) -> [T; 4] { 413 | [self.left, self.right, self.top, self.bottom] 414 | } 415 | } 416 | 417 | fn div_ceil(x: usize, y: usize) -> usize { 418 | if x % y > 0 { 419 | x / y + 1 420 | } else { 421 | x / y 422 | } 423 | } 424 | 425 | #[cfg(test)] 426 | mod tests { 427 | use proptest::prelude::*; 428 | use test_strategy::proptest; 429 | 430 | use crate::{ 431 | rect::{covered_area, obscured_area}, 432 | testing::{ContainedRects, ContainedRectsParams}, 433 | }; 434 | 435 | use super::*; 436 | 437 | #[test] 438 | fn remove_gaps_expands_at_same_rate() { 439 | let container = Size::new_checked(10, 10); 440 | let mut rects = arr1(&[ 441 | Rect::new_checked(2, 2, 6, 1), 442 | Rect::new_checked(2, 7, 1, 1), 443 | Rect::new_checked(7, 7, 1, 1), 444 | ]); 445 | remove_gaps(container, container, rects.view_mut()); 446 | assert_eq!( 447 | rects, 448 | arr1(&[ 449 | Rect::new_checked(0, 0, 10, 5), 450 | Rect::new_checked(0, 5, 5, 5), 451 | Rect::new_checked(5, 5, 5, 5), 452 | ]), 453 | ) 454 | } 455 | 456 | #[ignore = "fails when corners touch or rectangles overlap"] 457 | // Four or more rectangles can be in an arrangement 458 | // requiring overlapping 459 | // to remove gaps, 460 | // 461 | // ``` 462 | // aab 463 | // d b 464 | // dcc 465 | // ``` 466 | #[proptest] 467 | fn remove_gaps_with_no_max_size_and_1_to_3_rects_covers_container( 468 | #[strategy(ContainedRects::arbitrary_with(ContainedRectsParams::from_len_range(1..=3)))] 469 | args: ContainedRects, 470 | ) { 471 | let mut rects = Array::from(args.rects); 472 | remove_gaps(args.container, args.container, rects.view_mut()); 473 | prop_assert_eq!( 474 | covered_area(rects.as_slice().unwrap()), 475 | args.container.area() 476 | ) 477 | } 478 | 479 | #[ignore = "fails when corners touch"] 480 | // See above comment about four or more rects. 481 | #[proptest(max_global_rejects = 65536)] 482 | fn remove_gaps_with_1_to_3_rects_no_max_size_and_no_overlap_tiles_container( 483 | #[strategy(ContainedRects::arbitrary_with(ContainedRectsParams::from_len_range(1..=3)))] 484 | args: ContainedRects, 485 | ) { 486 | prop_assume!(obscured_area(&args.rects) == 0); 487 | let mut rects = Array::from(args.rects); 488 | remove_gaps(args.container, args.container, rects.view_mut()); 489 | prop_assert_eq!( 490 | rects.into_iter().map(|x| x.area().get()).sum::(), 491 | args.container.area().get() 492 | ) 493 | } 494 | 495 | #[ignore = "occasionally fails"] 496 | #[proptest(max_global_rejects = 65536)] 497 | fn remove_gaps_does_not_make_rects_overlap_if_they_did_not_already(args: RemoveGapsArgs) { 498 | prop_assume!(obscured_area(&args.rects) == 0); 499 | let mut rects = Array::from(args.rects); 500 | remove_gaps(args.max_size, args.container, rects.view_mut()); 501 | prop_assert_eq!(obscured_area(rects.as_slice().unwrap()), 0) 502 | } 503 | 504 | #[proptest] 505 | fn remove_gaps_respects_max_size(args: RemoveGapsArgs) { 506 | let mut rects = Array::from(args.rects); 507 | remove_gaps(args.max_size, args.container, rects.view_mut()); 508 | for rect in rects { 509 | prop_assert!(rect.width() <= args.max_size.width); 510 | prop_assert!(rect.height() <= args.max_size.height); 511 | } 512 | } 513 | 514 | #[proptest] 515 | fn overlap_borders_does_not_expand_past_container( 516 | #[strategy(1_usize..=32)] border_thickness: usize, 517 | container: Size, 518 | ) { 519 | let init = [Rect::new(0, 0, container.width, container.height)]; 520 | let mut rects = arr1(&init); 521 | overlap_borders(border_thickness, container, rects.view_mut()); 522 | assert_eq!(rects.into_raw_vec(), init) 523 | } 524 | 525 | #[test] 526 | fn overlap_borders_expands_evenly() { 527 | let container = Size::new_checked(10, 10); 528 | let mut rects = arr1(&[ 529 | Rect::new_checked(0, 0, 10, 5), 530 | Rect::new_checked(0, 5, 5, 5), 531 | Rect::new_checked(5, 5, 5, 5), 532 | ]); 533 | overlap_borders(2, container, rects.view_mut()); 534 | assert_eq!( 535 | rects, 536 | arr1(&[ 537 | Rect::new_checked(0, 0, 10, 6), 538 | Rect::new_checked(0, 4, 6, 6), 539 | Rect::new_checked(4, 4, 6, 6), 540 | ]), 541 | ) 542 | } 543 | 544 | #[test] 545 | fn overlap_borders_breaks_ties_in_favor_of_first() { 546 | let container = Size::new_checked(10, 10); 547 | let mut rects = arr1(&[ 548 | Rect::new_checked(0, 0, 10, 5), 549 | Rect::new_checked(0, 5, 5, 5), 550 | Rect::new_checked(5, 5, 5, 5), 551 | ]); 552 | overlap_borders(1, container, rects.view_mut()); 553 | assert_eq!( 554 | rects, 555 | arr1(&[ 556 | Rect::new_checked(0, 0, 10, 6), 557 | Rect::new_checked(0, 5, 6, 5), 558 | Rect::new_checked(5, 5, 5, 5), 559 | ]), 560 | ) 561 | } 562 | 563 | #[test] 564 | fn div_ceil_works_for_simple_cases() { 565 | assert_eq!(div_ceil(11, 2), 6); 566 | assert_eq!(div_ceil(3, 2), 2); 567 | assert_eq!(div_ceil(1, 2), 1); 568 | assert_eq!(div_ceil(0, 2), 0); 569 | assert_eq!(div_ceil(2, 2), 1); 570 | assert_eq!(div_ceil(4, 2), 2); 571 | assert_eq!(div_ceil(4, 3), 2); 572 | assert_eq!(div_ceil(5, 3), 2); 573 | assert_eq!(div_ceil(6, 3), 2); 574 | } 575 | 576 | #[derive(Clone, Debug)] 577 | struct RemoveGapsArgs { 578 | max_size: Size, 579 | container: Size, 580 | rects: Vec, 581 | } 582 | 583 | impl Arbitrary for RemoveGapsArgs { 584 | type Parameters = ContainedRectsParams; 585 | type Strategy = BoxedStrategy; 586 | 587 | fn arbitrary_with(range: Self::Parameters) -> Self::Strategy { 588 | ContainedRects::arbitrary_with(range) 589 | .prop_flat_map(|x| { 590 | ( 591 | ( 592 | x.rects.iter().map(|x| x.width().get()).max().unwrap_or(1) 593 | ..=x.container.width.get(), 594 | x.rects.iter().map(|x| x.height().get()).max().unwrap_or(1) 595 | ..=x.container.height.get(), 596 | ) 597 | .prop_map(|(width, height)| Size::new_checked(width, height)), 598 | Just(x), 599 | ) 600 | }) 601 | .prop_map(|(max_size, x)| Self { 602 | max_size, 603 | container: x.container, 604 | rects: x.rects, 605 | }) 606 | .boxed() 607 | } 608 | } 609 | } 610 | -------------------------------------------------------------------------------- /src/rect.rs: -------------------------------------------------------------------------------- 1 | use std::{cmp::PartialOrd, num::NonZeroUsize}; 2 | 3 | use itertools::Itertools; 4 | 5 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 6 | pub struct Rect { 7 | pub pos: Pos, 8 | pub size: Size, 9 | } 10 | 11 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 12 | pub struct Pos { 13 | pub x: usize, 14 | pub y: usize, 15 | } 16 | 17 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 18 | pub struct Size { 19 | pub width: NonZeroUsize, 20 | pub height: NonZeroUsize, 21 | } 22 | 23 | impl Rect { 24 | pub fn new(x: usize, y: usize, width: NonZeroUsize, height: NonZeroUsize) -> Self { 25 | Self { 26 | pos: Pos::new(x, y), 27 | size: Size::new(width, height), 28 | } 29 | } 30 | 31 | pub fn new_checked(x: usize, y: usize, width: usize, height: usize) -> Self { 32 | Self { 33 | pos: Pos::new(x, y), 34 | size: Size::new_checked(width, height), 35 | } 36 | } 37 | 38 | pub fn x(&self) -> usize { 39 | self.pos.x 40 | } 41 | 42 | pub fn y(&self) -> usize { 43 | self.pos.y 44 | } 45 | 46 | pub fn width(&self) -> NonZeroUsize { 47 | self.size.width 48 | } 49 | 50 | pub fn height(&self) -> NonZeroUsize { 51 | self.size.height 52 | } 53 | 54 | pub fn left(&self) -> usize { 55 | self.pos.x 56 | } 57 | 58 | pub fn right(&self) -> usize { 59 | self.pos.x + self.size.width.get() 60 | } 61 | 62 | pub fn top(&self) -> usize { 63 | self.pos.y 64 | } 65 | 66 | pub fn bottom(&self) -> usize { 67 | self.pos.y + self.size.height.get() 68 | } 69 | 70 | pub fn center(&self) -> Pos { 71 | Pos::new(self.center_x(), self.center_y()) 72 | } 73 | 74 | pub fn center_x(&self) -> usize { 75 | self.left() + self.size.width.get() / 2 76 | } 77 | 78 | pub fn center_y(&self) -> usize { 79 | self.top() + self.size.height.get() / 2 80 | } 81 | 82 | pub fn top_left(&self) -> Pos { 83 | Pos { 84 | y: self.top(), 85 | x: self.left(), 86 | } 87 | } 88 | 89 | pub fn top_right(&self) -> Pos { 90 | Pos { 91 | y: self.top(), 92 | x: self.right(), 93 | } 94 | } 95 | 96 | pub fn bottom_left(&self) -> Pos { 97 | Pos { 98 | y: self.bottom(), 99 | x: self.left(), 100 | } 101 | } 102 | 103 | pub fn bottom_right(&self) -> Pos { 104 | Pos { 105 | y: self.bottom(), 106 | x: self.right(), 107 | } 108 | } 109 | 110 | pub fn expand_left(&mut self, value: usize) { 111 | self.pos.x -= value; 112 | // Values should be small enough, 113 | // we do not expect overflow. 114 | self.size.width = unsafe { NonZeroUsize::new_unchecked(self.size.width.get() + value) }; 115 | } 116 | 117 | pub fn expand_right(&mut self, value: usize) { 118 | // Values should be small enough, 119 | // we do not expect overflow. 120 | self.size.width = unsafe { NonZeroUsize::new_unchecked(self.size.width.get() + value) }; 121 | } 122 | 123 | pub fn expand_top(&mut self, value: usize) { 124 | self.pos.y -= value; 125 | // Values should be small enough, 126 | // we do not expect overflow. 127 | self.size.height = unsafe { NonZeroUsize::new_unchecked(self.size.height.get() + value) }; 128 | } 129 | 130 | pub fn expand_bottom(&mut self, value: usize) { 131 | // Values should be small enough, 132 | // we do not expect overflow. 133 | self.size.height = unsafe { NonZeroUsize::new_unchecked(self.size.height.get() + value) }; 134 | } 135 | 136 | pub fn x_range_exclusive(&self) -> RangeExclusive { 137 | RangeExclusive(self.left(), self.right()) 138 | } 139 | 140 | pub fn y_range_exclusive(&self) -> RangeExclusive { 141 | RangeExclusive(self.top(), self.bottom()) 142 | } 143 | 144 | pub fn area(&self) -> NonZeroUsize { 145 | self.size.area() 146 | } 147 | 148 | pub fn overlap(&self, other: &Rect) -> Option { 149 | let left = self.left().max(other.left()); 150 | let right = self.right().min(other.right()); 151 | let top = self.top().max(other.top()); 152 | let bottom = self.bottom().min(other.bottom()); 153 | 154 | if left < right && top < bottom { 155 | Some(Rect { 156 | pos: Pos { x: left, y: top }, 157 | // We already checked 158 | // these values are not equal. 159 | size: Size { 160 | width: unsafe { NonZeroUsize::new_unchecked(right - left) }, 161 | height: unsafe { NonZeroUsize::new_unchecked(bottom - top) }, 162 | }, 163 | }) 164 | } else { 165 | None 166 | } 167 | } 168 | 169 | pub fn diff(self, other: Self) -> usize { 170 | self.pos.dist(other.pos) + self.size.diff(other.size) 171 | } 172 | } 173 | 174 | impl Pos { 175 | pub fn new(x: usize, y: usize) -> Self { 176 | Self { x, y } 177 | } 178 | 179 | /// Return manhattan distance between positions. 180 | pub fn dist(self, other: Pos) -> usize { 181 | (if self.x > other.x { 182 | self.x - other.x 183 | } else { 184 | other.x - self.x 185 | }) + (if self.y > other.y { 186 | self.y - other.y 187 | } else { 188 | other.y - self.y 189 | }) 190 | } 191 | } 192 | 193 | impl Size { 194 | pub fn new(width: NonZeroUsize, height: NonZeroUsize) -> Self { 195 | Self { width, height } 196 | } 197 | 198 | pub fn new_checked(width: usize, height: usize) -> Self { 199 | Self { 200 | width: width.try_into().unwrap(), 201 | height: height.try_into().unwrap(), 202 | } 203 | } 204 | 205 | pub fn area(&self) -> NonZeroUsize { 206 | // Area cannot be zero 207 | // if dimensions are not. 208 | unsafe { NonZeroUsize::new_unchecked(self.width.get() * self.height.get()) } 209 | } 210 | 211 | pub fn diff(self, other: Size) -> usize { 212 | Pos::from(self).dist(other.into()) 213 | } 214 | } 215 | 216 | impl From for Pos { 217 | fn from(value: Size) -> Self { 218 | Pos { 219 | x: value.width.get(), 220 | y: value.height.get(), 221 | } 222 | } 223 | } 224 | 225 | // Adapted from a solution by `m-hgn` on Code Wars, 226 | // . 227 | // This could be optimized using segment trees. 228 | /// Return the total area of a union of rectangles. 229 | pub fn covered_area(rects: &[Rect]) -> NonZeroUsize { 230 | let mut xs = rects 231 | .iter() 232 | .flat_map(|rect| [rect.left(), rect.right()]) 233 | .collect_vec(); 234 | xs.sort(); 235 | xs.dedup(); 236 | 237 | let mut rects = rects.to_vec(); 238 | rects.sort_by_key(|rect| rect.top()); 239 | 240 | let area = xs 241 | .into_iter() 242 | .tuple_windows() 243 | .map(|(left, right)| { 244 | let width = right - left; 245 | let mut last_y2 = usize::MIN; 246 | rects 247 | .iter() 248 | .filter(|rect| rect.left() <= left && right <= rect.right()) 249 | .map(|rect| { 250 | let ret = width * rect.bottom().saturating_sub(last_y2.max(rect.top())); 251 | last_y2 = rect.bottom().max(last_y2); 252 | ret 253 | }) 254 | .sum::() 255 | }) 256 | .sum(); 257 | // Covered area cannot be zero 258 | // because area of rectangles are not zero. 259 | unsafe { NonZeroUsize::new_unchecked(area) } 260 | } 261 | 262 | /// Return the total area obscured in a set of rectangles. 263 | /// If `n` rectangles are overlapped by an `n + 1`th rectangle, 264 | /// the overlapped area will be counted `n` times, 265 | /// but not `n + 1` times. 266 | pub fn obscured_area(rects: &[Rect]) -> usize { 267 | if rects.len() < 2 { 268 | 0 269 | } else { 270 | let overlaps = rects 271 | .iter() 272 | .enumerate() 273 | .map(|(i, rect)| { 274 | rects 275 | .iter() 276 | .enumerate() 277 | .filter(|(other_i, _)| i != *other_i) 278 | .filter_map(|(_, other)| rect.overlap(other)) 279 | .collect_vec() 280 | }) 281 | .collect_vec(); 282 | overlaps 283 | .iter() 284 | .map(|x| covered_area(x).get()) 285 | .sum::() 286 | - covered_area(&overlaps.into_iter().flatten().collect_vec()).get() 287 | } 288 | } 289 | 290 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 291 | pub struct RangeExclusive(pub T, pub T); 292 | 293 | impl RangeExclusive { 294 | pub fn intersects(self, other: RangeExclusive) -> bool 295 | where 296 | T: Copy + PartialOrd, 297 | { 298 | self == other || self.contains_either(other) || other.contains_either(self) 299 | } 300 | 301 | fn contains_either(self, other: RangeExclusive) -> bool 302 | where 303 | T: Copy + PartialOrd, 304 | { 305 | self.contains(other.0) || self.contains(other.1) 306 | } 307 | 308 | pub fn contains(self, x: T) -> bool 309 | where 310 | T: Copy + PartialOrd, 311 | { 312 | x > self.0 && x < self.1 313 | } 314 | } 315 | 316 | #[cfg(test)] 317 | mod tests { 318 | use proptest::prelude::*; 319 | use test_strategy::proptest; 320 | 321 | use super::*; 322 | 323 | #[test] 324 | fn range_exclusive_intersects_works_for_simple_cases() { 325 | assert!(RangeExclusive(0, 2).intersects(RangeExclusive(1, 2))); 326 | assert!(RangeExclusive(0, 3).intersects(RangeExclusive(1, 2))); 327 | assert!(!RangeExclusive(0, 1).intersects(RangeExclusive(1, 2))); 328 | } 329 | 330 | #[proptest] 331 | fn range_exclusive_intersects_with_itself(x: RangeExclusive) { 332 | prop_assert!(x.intersects(x)); 333 | } 334 | 335 | #[proptest] 336 | fn range_exclusive_intersects_is_symmetrical( 337 | x: RangeExclusive, 338 | y: RangeExclusive, 339 | ) { 340 | prop_assert_eq!(x.intersects(y), y.intersects(x)); 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /src/testing.rs: -------------------------------------------------------------------------------- 1 | use std::{num::NonZeroUsize, ops::RangeInclusive}; 2 | 3 | use proptest::prelude::{prop::collection::vec, *}; 4 | 5 | use crate::{rect::RangeExclusive, Rect, Size}; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct ContainedRects { 9 | pub container: Size, 10 | pub rects: Vec, 11 | } 12 | 13 | pub struct ContainedRectsParams { 14 | pub width_range: RangeInclusive, 15 | pub height_range: RangeInclusive, 16 | pub len_range: RangeInclusive, 17 | } 18 | 19 | impl Default for ContainedRectsParams { 20 | fn default() -> Self { 21 | Self { 22 | width_range: NonZeroUsize::new(1).unwrap()..=NonZeroUsize::new(5120).unwrap(), 23 | height_range: NonZeroUsize::new(1).unwrap()..=NonZeroUsize::new(2160).unwrap(), 24 | len_range: 0..=16, 25 | } 26 | } 27 | } 28 | 29 | impl ContainedRectsParams { 30 | pub fn from_len_range(range: RangeInclusive) -> Self { 31 | Self { 32 | len_range: range, 33 | ..Self::default() 34 | } 35 | } 36 | 37 | fn width_range_usize(&self) -> RangeInclusive { 38 | self.width_range.start().get()..=self.width_range.end().get() 39 | } 40 | 41 | fn height_range_usize(&self) -> RangeInclusive { 42 | self.height_range.start().get()..=self.height_range.end().get() 43 | } 44 | } 45 | 46 | impl Arbitrary for ContainedRects { 47 | type Parameters = ContainedRectsParams; 48 | type Strategy = BoxedStrategy; 49 | 50 | fn arbitrary_with(params: Self::Parameters) -> Self::Strategy { 51 | ( 52 | params.width_range_usize(), 53 | params.height_range_usize(), 54 | params.len_range, 55 | ) 56 | .prop_flat_map(|(width, height, count)| { 57 | vec( 58 | (0..width, 0..height).prop_flat_map(move |(x, y)| { 59 | (1..=width - x, 1..=height - y) 60 | .prop_map(move |(width, height)| Rect::new_checked(x, y, width, height)) 61 | }), 62 | count, 63 | ) 64 | .prop_map(move |rects| ContainedRects { 65 | container: Size::new_checked(width, height), 66 | rects, 67 | }) 68 | }) 69 | .boxed() 70 | } 71 | } 72 | 73 | impl Arbitrary for Size { 74 | type Parameters = (); 75 | type Strategy = BoxedStrategy; 76 | 77 | fn arbitrary_with(_: Self::Parameters) -> Self::Strategy { 78 | (1_usize..=5120, 1_usize..=2160) 79 | .prop_map(|(width, height)| Size::new_checked(width, height)) 80 | .boxed() 81 | } 82 | } 83 | 84 | impl Arbitrary for RangeExclusive 85 | where 86 | T: Arbitrary, 87 | T::Parameters: Clone, 88 | T::Strategy: 'static, 89 | { 90 | type Parameters = T::Parameters; 91 | type Strategy = BoxedStrategy; 92 | 93 | fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { 94 | (T::arbitrary_with(args.clone()), T::arbitrary_with(args)) 95 | .prop_map(|(x, y)| RangeExclusive(x, y)) 96 | .boxed() 97 | } 98 | } 99 | --------------------------------------------------------------------------------