kasuari-0.4.11/.cargo_vcs_info.json0000644000000001360000000000100125560ustar { "git": { "sha1": "dfde5428e6ad7ab0eebaea89be940246e3e523b8" }, "path_in_vcs": "" }kasuari-0.4.11/.github/dependabot.yml000064400000000000000000000006541046102023000155430ustar 00000000000000version: 2 updates: # Maintain dependencies for Cargo - package-ecosystem: "cargo" directory: "/" schedule: interval: "weekly" groups: rust-dependencies: patterns: - "*" # Maintain dependencies for GitHub Actions - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" groups: github-actions: patterns: - "*" kasuari-0.4.11/.github/workflows/ci.yml000064400000000000000000000045111046102023000160620ustar 00000000000000name: Continuous Integration on: push: branches: [main] pull_request: branches: [main] permissions: contents: read jobs: build: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - name: std (windows) os: windows-2022 target: x86_64-pc-windows-msvc args: "" run_tests: true - name: std (macos) os: macos-latest target: x86_64-apple-darwin args: "" run_tests: true - name: std (linux) os: ubuntu-latest target: x86_64-unknown-linux-gnu args: "" run_tests: true - name: no_std (thumbv7em) os: ubuntu-latest target: thumbv7em-none-eabi args: "--no-default-features" run_tests: false - name: no_std (riscv32 + portable-atomic) os: ubuntu-latest target: riscv32imc-unknown-none-elf args: "--no-default-features --features portable-atomic,portable-atomic/unsafe-assume-single-core" run_tests: false name: ${{ matrix.name }} steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@stable with: target: ${{ matrix.target }} - name: Build run: cargo build --target ${{ matrix.target }} ${{ matrix.args }} - name: Test if: matrix.run_tests run: cargo test --target ${{ matrix.target }} clippy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@stable - run: cargo clippy --all-targets --all-features -- -D warnings fmt: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@nightly with: components: rustfmt - run: cargo fmt -- --check coverage: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@stable with: components: llvm-tools - uses: taiki-e/install-action@cargo-llvm-cov - run: cargo llvm-cov --lcov --output-path target/lcov.info - uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} slug: ratatui/kasuari fail_ci_if_error: false kasuari-0.4.11/.github/workflows/release-plz.yml000064400000000000000000000023601046102023000177120ustar 00000000000000name: Release-plz on: push: branches: [main] workflow_dispatch: jobs: release: name: Release-plz release if: ${{ github.repository_owner == 'ratatui' }} runs-on: ubuntu-latest environment: release permissions: contents: write id-token: write steps: - &checkout name: Checkout repository uses: actions/checkout@v6 with: fetch-depth: 0 persist-credentials: false - &install-rust name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Run release-plz (release) uses: release-plz/action@v0.5 with: command: release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} release_pr: name: Release-plz PR if: ${{ github.repository_owner == 'ratatui' }} runs-on: ubuntu-latest permissions: contents: write pull-requests: write concurrency: group: release-plz-${{ github.ref }} cancel-in-progress: false steps: - *checkout - *install-rust - name: Run release-plz (release-pr) uses: release-plz/action@v0.5 with: command: release-pr env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} kasuari-0.4.11/.gitignore000064400000000000000000000000331046102023000133320ustar 00000000000000target /.idea/ /*.iml .env kasuari-0.4.11/CHANGELOG.md000064400000000000000000000134731046102023000131670ustar 00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.4.11](https://github.com/ratatui/kasuari/compare/v0.4.10...v0.4.11) - 2025-11-29 ### Other - *(deps)* bump actions/checkout from 5 to 6 in the github-actions group ([#42](https://github.com/ratatui/kasuari/pull/42)) - *(deps)* bump hashbrown from 0.16.0 to 0.16.1 in the rust-dependencies group ([#43](https://github.com/ratatui/kasuari/pull/43)) - enable trusted publishing and normalize release-plz workflow ([#44](https://github.com/ratatui/kasuari/pull/44)) ## [0.4.10](https://github.com/ratatui/kasuari/compare/v0.4.9...v0.4.10) - 2025-11-02 ### Other - *(deps)* bump thiserror from 2.0.16 to 2.0.17 in the rust-dependencies group ([#36](https://github.com/ratatui/kasuari/pull/36)) - *(features)* improve feature flags docs ([#35](https://github.com/ratatui/kasuari/pull/35)) - Fixed a bug that lead to the leaking of `Symbol`s in the objective function. ([#37](https://github.com/ratatui/kasuari/pull/37)) - Bump MSRV to 1.85 to use clamp functions in const context ([#39](https://github.com/ratatui/kasuari/pull/39)) ### Changed - bump the MSRV to 1.85 and rely on the standard `f64::clamp` in const contexts ## [0.4.9](https://github.com/ratatui/kasuari/compare/v0.4.8...v0.4.9) - 2025-09-04 ### Added - always use `std` atomic types when using `--all-features` ([#33](https://github.com/ratatui/kasuari/pull/33)) ## [0.4.8](https://github.com/ratatui/kasuari/compare/v0.4.7...v0.4.8) - 2025-09-04 ### Other - *(deps)* bump the rust-dependencies group across 1 directory with 2 updates ([#32](https://github.com/ratatui/kasuari/pull/32)) - add smoke tests for no_std targets ([#31](https://github.com/ratatui/kasuari/pull/31)) - use portable-atomic to allow use on targets without atomic instructions ([#30](https://github.com/ratatui/kasuari/pull/30)) - *(deps)* bump actions/checkout from 4 to 5 in the github-actions group ([#27](https://github.com/ratatui/kasuari/pull/27)) - *(deps)* bump the rust-dependencies group with 2 updates ([#28](https://github.com/ratatui/kasuari/pull/28)) - *(deps)* bump rstest from 0.25.0 to 0.26.1 in the rust-dependencies group ([#25](https://github.com/ratatui/kasuari/pull/25)) # Changelog ## [0.4.7](https://github.com/ratatui/kasuari/compare/v0.4.6...v0.4.7) - 2025-06-27 ### Other - *(gitignore)* add `.env` to `.gitignore` ([#22](https://github.com/ratatui/kasuari/pull/22)) - use variables directly in the `format!` string ([#23](https://github.com/ratatui/kasuari/pull/23)) - *(deps)* bump hashbrown from 0.15.3 to 0.15.4 in the rust-dependencies group ([#19](https://github.com/ratatui/kasuari/pull/19)) - fix for code scanning alert no. 1: Workflow does not contain permissions ([#20](https://github.com/ratatui/kasuari/pull/20)) ## [0.4.6](https://github.com/ratatui/kasuari/compare/v0.4.5...v0.4.6) - 2025-05-08 ### Added - *(no_std)* option to disable `std` ([#16](https://github.com/ratatui/kasuari/pull/16)) ## [0.4.5](https://github.com/ratatui/kasuari/compare/v0.4.4...v0.4.5) - 2025-05-06 ### Other - fix repo links joshka->ratatui org ([#14](https://github.com/ratatui/kasuari/pull/14)) ## [0.4.4](https://github.com/ratatui/kasuari/compare/v0.4.3...v0.4.4) - 2025-05-06 ### Other - remove unused deps ([#12](https://github.com/ratatui/kasuari/pull/12)) ## [0.4.3](https://github.com/ratatui/kasuari/compare/v0.4.2...v0.4.3) - 2025-04-04 ### Other - use the same formatting as ratatui ([#9](https://github.com/ratatui/kasuari/pull/9)) ## [0.4.2](https://github.com/ratatui/kasuari/compare/v0.4.1...v0.4.2) - 2025-04-04 ### Other - fix release-plz check to ratatui-org - move to ratatui github org ([#7](https://github.com/ratatui/kasuari/pull/7)) - configure dependabot - add coverage ## [0.4.1](https://github.com/ratatui/kasuari/compare/v0.4.0...v0.4.1) - 2025-04-04 ### Other - fix build badge ## [0.4.0](https://github.com/ratatui/kasuari/compare/v0.4.0-alpha.2...v0.4.0) - 2025-04-04 ### Added - add const methods for Strength ops - make Strength new() and create() const ## [0.4.0-alpha.2](https://github.com/ratatui/kasuari/compare/v0.4.0-alpha.1...v0.4.0-alpha.2) - 2025-04-04 ### Added - make the crate `no_std` ([#2](https://github.com/ratatui/kasuari/pull/2)) ### Fixed - clippy lints - doc tests ### Other - add release-plz automation - rename ci workflow - tweak cargo.toml, unignore cargo.lock - add simple ci workflow ## Kasuari 0.4.0-alpha.1 This release is a fork of the library under a new name, `Kasuari`. The name change is to avoid confusion with the original `Cassowary-rs` library, which has been unmaintained since 2018. The name `Kasuari` is the Indonesian name for the Cassowary bird. - Initial Kasuari release - Update to Rust 2021 edition - Update docs - Reformat code - Cleanup lints - Move code to appropriate modules - Add `Debug` implementations for various types - Implement Error for errors and provide better error messages - Add error source to InternalSolverError for better debugging - Add tests - Spell optimise with US english (optimize) - Make Strength a newtype instead of f64 - Pass constrains by value instead of reference ## Casssowary 0.3.0 - Various fixes (PR #4) from @christolliday. > Main breaking change is that variables no longer silently initialise to zero and will report their initial value in the first call to `fetch_changes`, also `has_edit_variable` now takes `&self` instead of `&mut self`. ## Casssowary 0.2.1 - Fixed crash under certain use cases. See PR #1 (Thanks @christolliday!). ## Casssowary 0.2.0 - Changed API to only report changes to the values of variables. This allows for more efficient use of the library in typical applications. ## Casssowary 0.1 Initial release kasuari-0.4.11/Cargo.lock0000644000000223500000000000100105330ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "allocator-api2" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "document-features" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "foldhash" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-macro" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "futures-task" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-timer" version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-macro", "futures-task", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "glob" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", "foldhash", ] [[package]] name = "indexmap" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" dependencies = [ "equivalent", "hashbrown 0.15.5", ] [[package]] name = "kasuari" version = "0.4.11" dependencies = [ "document-features", "hashbrown 0.16.1", "portable-atomic", "portable-atomic-util", "rstest", "thiserror", ] [[package]] name = "litrs" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "portable-atomic" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portable-atomic-util" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" dependencies = [ "portable-atomic", ] [[package]] name = "proc-macro-crate" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "relative-path" version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "rstest" version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" dependencies = [ "futures-timer", "futures-util", "rstest_macros", ] [[package]] name = "rstest_macros" version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" dependencies = [ "cfg-if", "glob", "proc-macro-crate", "proc-macro2", "quote", "regex", "relative-path", "rustc_version", "syn", "unicode-ident", ] [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "semver" version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "syn" version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] name = "toml_edit" version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ "indexmap", "toml_datetime", "winnow", ] [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "winnow" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" dependencies = [ "memchr", ] kasuari-0.4.11/Cargo.toml0000644000000041370000000000100105610ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.85" name = "kasuari" version = "0.4.11" authors = [ "Dylan Ede ", "The Ratatui Developers", ] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = """ A rust layout solver for GUIs, based on the Cassowary algorithm. A fork of the unmaintained cassowary-rs crate with improvments and bug fixes. Kasuari is the indonesian name for the cassowary bird. """ documentation = "https://docs.rs/kasuari" readme = "README.md" keywords = [ "cassowary", "layout", "tui", "solver", ] categories = [ "algorithms", "command-line-interface", "graphics", "gui", "rendering", ] license = "MIT OR Apache-2.0" repository = "https://github.com/ratatui/kasuari" [package.metadata.docs.rs] all-features = true [features] default = ["std"] document-features = ["dep:document-features"] portable-atomic = [ "dep:portable-atomic", "dep:portable-atomic-util", ] std = [ "thiserror/std", "portable-atomic?/std", ] [lib] name = "kasuari" path = "src/lib.rs" [[test]] name = "quadrilateral" path = "tests/quadrilateral.rs" [[test]] name = "removal" path = "tests/removal.rs" [dependencies.document-features] version = "0.2" optional = true [dependencies.hashbrown] version = "0.16" [dependencies.portable-atomic] version = "1.11" features = ["require-cas"] optional = true default-features = false [dependencies.portable-atomic-util] version = "0.2.4" features = ["alloc"] optional = true [dependencies.thiserror] version = "2.0" default-features = false [dev-dependencies.rstest] version = "0.26" kasuari-0.4.11/Cargo.toml.orig000064400000000000000000000033361046102023000142420ustar 00000000000000[package] name = "kasuari" version = "0.4.11" authors = ["Dylan Ede ", "The Ratatui Developers"] edition = "2021" description = """ A rust layout solver for GUIs, based on the Cassowary algorithm. A fork of the unmaintained cassowary-rs crate with improvments and bug fixes. Kasuari is the indonesian name for the cassowary bird. """ documentation = "https://docs.rs/kasuari" repository = "https://github.com/ratatui/kasuari" readme = "README.md" license = "MIT OR Apache-2.0" keywords = ["cassowary", "layout", "tui", "solver"] categories = [ "algorithms", "command-line-interface", "graphics", "gui", "rendering", ] rust-version = "1.85" [features] default = ["std"] ## Enables usage of standard library. std = ["thiserror/std", "portable-atomic?/std"] ## Enables fallback implementations of atomic types on targets without native support. ## Requires adding `portable-atomic` crate as a dependency and setting proper `portable-atomic` feature flags by the ## user based on target platform. ## See [`portable-atomic` crate documentation] for more information. ## ## [`portable-atomic` crate documentation]: https://docs.rs/portable-atomic/latest/portable_atomic portable-atomic = ["dep:portable-atomic", "dep:portable-atomic-util"] document-features = ["dep:document-features"] [dependencies] hashbrown = "0.16" portable-atomic = { version = "1.11", default-features = false, features = ["require-cas"], optional = true } portable-atomic-util = { version = "0.2.4", features = ["alloc"], optional = true } thiserror = { version = "2.0", default-features = false } document-features = { version = "0.2", optional = true } [package.metadata.docs.rs] all-features = true [dev-dependencies] rstest = "0.26" kasuari-0.4.11/LICENSE-APACHE000064400000000000000000000261351046102023000133010ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. kasuari-0.4.11/LICENSE-MIT000064400000000000000000000021251046102023000130020ustar 00000000000000The MIT License (MIT) Copyright (c) 2016 Dylan Ede Copyright (c) 2024 Josh McKinney Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. kasuari-0.4.11/README.md000064400000000000000000000056211046102023000126310ustar 00000000000000# Kasuari [![crate-badge]][crate] [![docs-badge]][docs] [![license-badge]][license] \ [![github-badge]][github] [![build-badge]][build] [![codecov-badge]][codecov] [crate-badge]: https://img.shields.io/crates/v/kasuari?logo=rust [docs-badge]: https://img.shields.io/badge/docs.rs-kasuari-blue?logo=rust [license-badge]: https://img.shields.io/crates/l/kasuari?logo=apache [github-badge]: https://img.shields.io/badge/github-ratatui%2Fkasuari-blue?logo=github [build-badge]: https://github.com/ratatui/kasuari/actions/workflows/ci.yml/badge.svg?logo=github [codecov-badge]: https://img.shields.io/codecov/c/github/ratatui/kasuari?logo=codecov [github]: https://github.com/ratatui/kasuari [crate]: https://crates.io/crates/kasuari [license]: #license [docs]: https://docs.rs/kasuari [build]: https://github.com/ratatui/kasuari/actions/workflows/ci.yml [codecov]: https://codecov.io/gh/ratatui/kasuari A Rust implementation of the Cassowary constraint solving algorithm ([Badros et. al 2001]). It is based heavily on the implementation the C++ [Kiwi] library. The implementation does however differ in some details. This library is a fork of [Cassowary-rs], by Dylan Ede, which hasn't been maintained since 2018. `Kasuari` is the Indonesian name for the Cassowary bird. Cassowary is designed for solving constraints to lay out user interfaces. Constraints typically take the form "this button must line up with this text box", or "this box should try to be 3 times the size of this other box". Its most popular incarnation by far is in Apple's Autolayout system for Mac OS X and iOS user interfaces. UI libraries using the Cassowary algorithm manage to achieve a much more natural approach to specifying UI layouts than traditional approaches like those found in HTML. This library is a low level interface to the solving algorithm, though it tries to be as convenient as possible. As a result it does not have any intrinsic knowledge of common user interface conventions like rectangular regions or even two dimensions. These abstractions belong in a higher level crate. For more information, please read the [Kasuari API docs]. ## Getting Started Add this crate to your Cargo.toml file ```shell cargo add kasuari ``` ## Changes from Cassowary-rs See the [CHANGELOG](./CHANGELOG.md) for a full list of changes. ## License Licensed under either of - Apache License, Version 2.0, ([LICENSE-APACHE](./LICENSE-APACHE) - MIT license ([LICENSE-MIT](./LICENSE-MIT) at your option. ### Contribution Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. [Badros et. al 2001]: https://constraints.cs.washington.edu/solvers/cassowary-tochi.pdf [Kiwi]: https://github.com/nucleic/kiwi [Cassowary-rs]: https://crates.io/crates/cassowary [Kasuari API docs]: https://docs.rs/kasuari kasuari-0.4.11/rustfmt.toml000064400000000000000000000004711046102023000137510ustar 00000000000000# configuration for https://rust-lang.github.io/rustfmt/ use_field_init_shorthand = true # unstable options comment_width = 100 format_code_in_doc_comments = true format_macro_matchers = true group_imports = "StdExternalCrate" imports_granularity = "Module" normalize_doc_attributes = true wrap_comments = true kasuari-0.4.11/src/constraint.rs000064400000000000000000000075601046102023000146770ustar 00000000000000#[cfg(not(feature = "portable-atomic"))] use alloc::sync::Arc; use core::hash::{Hash, Hasher}; use core::ops; #[cfg(feature = "portable-atomic")] use portable_atomic_util::Arc; use crate::{Expression, RelationalOperator, Strength, Term, Variable, WeightedRelation}; #[derive(Debug)] struct Inner { expression: Expression, strength: Strength, operator: RelationalOperator, } /// A constraint, consisting of an equation governed by an expression and a relational operator, /// and an associated strength. #[derive(Clone, Debug)] pub struct Constraint { inner: Arc, } impl Constraint { /// Construct a new constraint from an expression, a relational operator and a strength. /// This corresponds to the equation `e op 0.0`, e.g. `x + y >= 0.0`. For equations with a /// non-zero right hand side, subtract it from the equation to give a zero right hand side. pub fn new( expression: Expression, operator: RelationalOperator, strength: Strength, ) -> Constraint { Constraint { inner: Arc::new(Inner { expression, operator, strength, }), } } /// The expression of the left hand side of the constraint equation. pub fn expr(&self) -> &Expression { &self.inner.expression } /// The relational operator governing the constraint. pub fn op(&self) -> RelationalOperator { self.inner.operator } /// The strength of the constraint that the solver will use. pub fn strength(&self) -> Strength { self.inner.strength } } impl Hash for Constraint { fn hash(&self, hasher: &mut H) { use core::ops::Deref; hasher.write_usize(self.inner.deref() as *const _ as usize); } } impl PartialEq for Constraint { fn eq(&self, other: &Constraint) -> bool { use core::ops::Deref; core::ptr::eq(self.inner.deref(), other.inner.deref()) } } impl Eq for Constraint {} /// This is an intermediate type used in the syntactic sugar for specifying constraints. You should /// not use it directly. pub struct PartialConstraint { expression: Expression, relation: WeightedRelation, } impl PartialConstraint { /// Construct a new partial constraint from an expression and a relational operator. pub const fn new(expression: Expression, relation: WeightedRelation) -> PartialConstraint { PartialConstraint { expression, relation, } } } impl ops::BitOr for PartialConstraint { type Output = Constraint; fn bitor(self, rhs: f64) -> Constraint { let (operator, strength) = self.relation.into(); #[allow(clippy::suspicious_arithmetic_impl)] Constraint::new(self.expression - rhs, operator, strength) } } impl ops::BitOr for PartialConstraint { type Output = Constraint; fn bitor(self, rhs: f32) -> Constraint { self.bitor(rhs as f64) } } impl ops::BitOr for PartialConstraint { type Output = Constraint; fn bitor(self, rhs: Variable) -> Constraint { let (operator, strength) = self.relation.into(); #[allow(clippy::suspicious_arithmetic_impl)] Constraint::new(self.expression - rhs, operator, strength) } } impl ops::BitOr for PartialConstraint { type Output = Constraint; fn bitor(self, rhs: Term) -> Constraint { let (operator, strength) = self.relation.into(); #[allow(clippy::suspicious_arithmetic_impl)] Constraint::new(self.expression - rhs, operator, strength) } } impl ops::BitOr for PartialConstraint { type Output = Constraint; fn bitor(self, rhs: Expression) -> Constraint { let (operator, strength) = self.relation.into(); #[allow(clippy::suspicious_arithmetic_impl)] Constraint::new(self.expression - rhs, operator, strength) } } kasuari-0.4.11/src/error.rs000064400000000000000000000066041046102023000136420ustar 00000000000000use thiserror::Error; use crate::InternalSolverError; /// The possible error conditions that `Solver::add_constraint` can fail with. #[derive(Debug, Copy, Clone, Error)] pub enum AddConstraintError { /// The constraint specified has already been added to the solver. #[error("The constraint specified has already been added to the solver.")] DuplicateConstraint, /// The constraint is required, but it is unsatisfiable in conjunction with the existing /// constraints. #[error("The constraint is required, but it is unsatisfiable in conjunction with the existing constraints.")] UnsatisfiableConstraint, /// The solver entered an invalid state. #[error("The solver entered an invalid state. If this occurs please report the issue.")] InternalSolverError(#[from] InternalSolverError), } /// The possible error conditions that `Solver::remove_constraint` can fail with. #[derive(Debug, Copy, Clone, Error)] pub enum RemoveConstraintError { /// The constraint specified was not already in the solver, so cannot be removed. #[error("The constraint specified was not already in the solver, so cannot be removed.")] UnknownConstraint, /// The solver entered an invalid state. If this occurs please report the issue. This variant /// specifies additional details as a string. #[error("The solver entered an invalid state. If this occurs please report the issue.")] InternalSolverError(#[from] InternalSolverError), } /// The possible error conditions that `Solver::add_edit_variable` can fail with. #[derive(Debug, Copy, Clone, Error)] pub enum AddEditVariableError { /// The specified variable is already marked as an edit variable in the solver. #[error("The specified variable is already marked as an edit variable in the solver.")] DuplicateEditVariable, /// The specified strength was `REQUIRED`. This is illegal for edit variable strengths. #[error("The specified strength was `REQUIRED`. This is illegal for edit variable strengths.")] BadRequiredStrength, } /// The possible error conditions that `Solver::remove_edit_variable` can fail with. #[derive(Debug, Copy, Clone, Error)] pub enum RemoveEditVariableError { /// The specified variable was not an edit variable in the solver, so cannot be removed. #[error( "The specified variable was not an edit variable in the solver, so cannot be removed." )] UnknownEditVariable, /// The solver entered an invalid state. If this occurs please report the issue. This variant /// specifies additional details as a string. #[error("The solver entered an invalid state. If this occurs please report the issue.")] InternalSolverError(#[from] InternalSolverError), } /// The possible error conditions that `Solver::suggest_value` can fail with. #[derive(Debug, Copy, Clone, Error)] pub enum SuggestValueError { /// The specified variable was not an edit variable in the solver, so cannot have its value /// suggested. #[error( "The specified variable was not an edit variable in the solver, so cannot have its value suggested." )] UnknownEditVariable, /// The solver entered an invalid state. If this occurs please report the issue. This variant /// specifies additional details as a string. #[error("The solver entered an invalid state. If this occurs please report the issue.")] InternalSolverError(#[from] InternalSolverError), } kasuari-0.4.11/src/expression.rs000064400000000000000000000165401046102023000147100ustar 00000000000000use alloc::vec; use alloc::vec::Vec; use core::ops; use crate::{Term, Variable}; /// An expression that can be the left hand or right hand side of a constraint equation. /// /// It is a linear combination of variables, i.e., a sum of variables weighted by coefficients, plus /// an optional constant. /// /// ```text /// expression = term_1 + term_2 + ... + term_n + constant /// ``` #[derive(Debug, Clone, PartialEq)] pub struct Expression { /// The terms in the expression. pub terms: Vec, /// The constant in the expression. pub constant: f64, } impl Expression { /// Create a new Expression. /// /// ```text /// expression = term_1 + term_2 + ... + term_n + constant /// ``` #[inline] pub const fn new(terms: Vec, constant: f64) -> Expression { Expression { terms, constant } } /// Constructs an expression that represents a constant without any terms /// /// ```text /// expression = constant /// ``` #[inline] pub const fn from_constant(constant: f64) -> Expression { Expression { terms: Vec::new(), constant, } } /// Constructs an expression from a single term. /// /// ```text /// expression = term /// ``` #[inline] pub fn from_term(term: Term) -> Expression { Expression { terms: vec![term], constant: 0.0, } } /// Constructs an expression from a terms /// /// ```text /// expression = term_1 + term_2 + ... + term_n /// ``` #[inline] pub const fn from_terms(terms: Vec) -> Expression { Expression { terms, constant: 0.0, } } /// Constructs an expression from a variable /// /// ```text /// expression = variable /// ``` pub fn from_variable(variable: Variable) -> Expression { Expression { terms: vec![Term::from_variable(variable)], constant: 0.0, } } } impl From for Expression { #[inline] fn from(constant: f64) -> Expression { Expression::from_constant(constant) } } impl From for Expression { #[inline] fn from(variable: Variable) -> Expression { let term = Term::from(variable); Expression::from_term(term) } } impl From for Expression { #[inline] fn from(term: Term) -> Expression { Expression::from_term(term) } } impl FromIterator for Expression { #[inline] fn from_iter>(iter: I) -> Self { let terms = iter.into_iter().collect(); Expression::from_terms(terms) } } impl ops::Neg for Expression { type Output = Expression; #[inline] fn neg(self) -> Expression { Expression { terms: self.terms.iter().copied().map(Term::neg).collect(), constant: -self.constant, } } } impl ops::Mul for Expression { type Output = Expression; #[inline] fn mul(mut self, rhs: f64) -> Expression { self *= rhs; self } } impl ops::MulAssign for Expression { #[inline] fn mul_assign(&mut self, rhs: f64) { self.constant *= rhs; for term in &mut self.terms { *term = *term * rhs; } } } impl ops::Mul for Expression { type Output = Expression; #[inline] fn mul(self, rhs: f32) -> Expression { self * rhs as f64 } } impl ops::MulAssign for Expression { #[inline] fn mul_assign(&mut self, rhs: f32) { *self *= rhs as f64; } } impl ops::Mul for f64 { type Output = Expression; #[inline] fn mul(self, mut rhs: Expression) -> Expression { rhs.constant *= self; for term in &mut rhs.terms { *term *= self; } rhs } } impl ops::Mul for f32 { type Output = Expression; #[inline] fn mul(self, rhs: Expression) -> Expression { self as f64 * rhs } } impl ops::Div for Expression { type Output = Expression; #[inline] fn div(mut self, rhs: f64) -> Expression { self /= rhs; self } } impl ops::DivAssign for Expression { #[inline] fn div_assign(&mut self, rhs: f64) { self.constant /= rhs; for term in &mut self.terms { *term = *term / rhs; } } } impl ops::Div for Expression { type Output = Expression; #[inline] fn div(self, rhs: f32) -> Expression { self.div(rhs as f64) } } impl ops::DivAssign for Expression { #[inline] fn div_assign(&mut self, v: f32) { self.div_assign(v as f64) } } impl ops::Add for Expression { type Output = Expression; #[inline] fn add(mut self, rhs: f64) -> Expression { self += rhs; self } } impl ops::AddAssign for Expression { #[inline] fn add_assign(&mut self, rhs: f64) { self.constant += rhs; } } impl ops::Add for Expression { type Output = Expression; #[inline] fn add(self, rhs: f32) -> Expression { self.add(rhs as f64) } } impl ops::AddAssign for Expression { #[inline] fn add_assign(&mut self, rhs: f32) { self.add_assign(rhs as f64) } } impl ops::Add for f64 { type Output = Expression; #[inline] fn add(self, mut rhs: Expression) -> Expression { rhs.constant += self; rhs } } impl ops::Add for f32 { type Output = Expression; #[inline] fn add(self, rhs: Expression) -> Expression { (self as f64).add(rhs) } } impl ops::Add for Expression { type Output = Expression; #[inline] fn add(mut self, rhs: Expression) -> Expression { self += rhs; self } } impl ops::AddAssign for Expression { #[inline] fn add_assign(&mut self, mut rhs: Expression) { self.terms.append(&mut rhs.terms); self.constant += rhs.constant; } } impl ops::Sub for Expression { type Output = Expression; #[inline] fn sub(mut self, rhs: f64) -> Expression { self -= rhs; self } } impl ops::SubAssign for Expression { #[inline] fn sub_assign(&mut self, rhs: f64) { self.constant -= rhs; } } impl ops::Sub for Expression { type Output = Expression; #[inline] fn sub(self, rhs: f32) -> Expression { self.sub(rhs as f64) } } impl ops::SubAssign for Expression { #[inline] fn sub_assign(&mut self, rhs: f32) { self.sub_assign(rhs as f64) } } impl ops::Sub for f64 { type Output = Expression; #[inline] fn sub(self, mut rhs: Expression) -> Expression { rhs = -rhs; rhs.constant += self; rhs } } impl ops::Sub for f32 { type Output = Expression; #[inline] fn sub(self, rhs: Expression) -> Expression { (self as f64).sub(rhs) } } impl ops::Sub for Expression { type Output = Expression; #[inline] fn sub(mut self, rhs: Expression) -> Expression { self -= rhs; self } } impl ops::SubAssign for Expression { #[inline] fn sub_assign(&mut self, mut rhs: Expression) { rhs = -rhs; self.terms.append(&mut rhs.terms); self.constant += rhs.constant; } } kasuari-0.4.11/src/lib.rs000064400000000000000000000231531046102023000132550ustar 00000000000000//! This crate contains an implementation of the Cassowary constraint solving algorithm, based upon //! the work by G.J. Badros et al. in 2001. This algorithm is designed primarily for use //! constraining elements in user interfaces. Constraints are linear combinations of the problem //! variables. The notable features of Cassowary that make it ideal for user interfaces are that it //! is incremental (i.e. you can add and remove constraints at runtime and it will perform the //! minimum work to update the result) and that the constraints can be violated if necessary, with //! the order in which they are violated specified by setting a "strength" for each constraint. This //! allows the solution to gracefully degrade, which is useful for when a user interface needs to //! compromise on its constraints in order to still be able to display something. //! //! ## Constraint syntax //! //! This crate aims to provide syntax for describing linear constraints as naturally as possible, //! within the limitations of Rust's type system. Generally you can write constraints as you would //! naturally, however the operator symbol (for greater-than, less-than, equals) is replaced with an //! instance of the `WeightedRelation` enum wrapped in "pipe brackets". //! //! For example, for the constraint `(a + b) * 2 + c >= d + 1` with strength `s`, the code to use is //! //! ```ignore //! (a + b) * 2.0 + c |GE(s)| d + 1.0 //! ``` //! //! # A simple example //! //! Imagine a layout consisting of two elements laid out horizontally. For small window widths the //! elements should compress to fit, but if there is enough space they should display at their //! preferred widths. The first element will align to the left, and the second to the right. For //! this example we will ignore vertical layout. //! //! First we need to include the relevant parts of `cassowary`: //! //! ``` //! use kasuari::WeightedRelation::*; //! use kasuari::{Solver, Variable}; //! ``` //! //! And we'll construct some conveniences for pretty printing (which should hopefully be //! self-explanatory): //! //! ```ignore //! use hashbrown::HashMap; //! let mut names = HashMap::new(); //! fn print_changes(names: &HashMap, changes: &[(Variable, f64)]) { //! println!("Changes:"); //! for &(ref var, ref val) in changes { //! println!("{}: {}", names[var], val); //! } //! } //! ``` //! //! Let's define the variables required - the left and right edges of the elements, and the width of //! the window. //! //! ```ignore //! let window_width = Variable::new(); //! names.insert(window_width, "window_width"); //! //! struct Element { //! left: Variable, //! right: Variable //! } //! let box1 = Element { //! left: Variable::new(), //! right: Variable::new() //! }; //! names.insert(box1.left, "box1.left"); //! names.insert(box1.right, "box1.right"); //! //! let box2 = Element { //! left: Variable::new(), //! right: Variable::new() //! }; //! names.insert(box2.left, "box2.left"); //! names.insert(box2.right, "box2.right"); //! ``` //! //! Now to set up the solver and constraints. //! //! ```ignore //! let mut solver = Solver::new(); //! solver.add_constraints(&[ //! window_width |GE(REQUIRED)| 0.0, // positive window width //! box1.left |EQ(REQUIRED)| 0.0, // left align //! box2.right |EQ(REQUIRED)| window_width, // right align //! box2.left |GE(REQUIRED)| box1.right, // no overlap //! // positive widths //! box1.left |LE(REQUIRED)| box1.right, //! box2.left |LE(REQUIRED)| box2.right, //! // preferred widths: //! box1.right - box1.left |EQ(WEAK)| 50.0, //! box2.right - box2.left |EQ(WEAK)| 100.0 //! ])?; //! # Ok::<(), kasuari::InternalSolverError>(()) //! ``` //! //! The window width is currently free to take any positive value. Let's constrain it to a //! particular value. Since for this example we will repeatedly change the window width, it is most //! efficient to use an "edit variable", instead of repeatedly removing and adding constraints (note //! that for efficiency reasons we cannot edit a normal constraint that has been added to the //! solver). //! //! ```ignore //! solver.add_edit_variable(window_width, STRONG).unwrap(); //! solver.suggest_value(window_width, 300.0).unwrap(); //! ``` //! //! This value of 300 is enough to fit both boxes in with room to spare, so let's check that this is //! the case. We can fetch a list of changes to the values of variables in the solver. Using the //! pretty printer defined earlier we can see what values our variables now hold. //! //! ```ignore //! print_changes(&names, solver.fetch_changes()); //! ``` //! //! This should print (in a possibly different order): //! //! ```ignore //! Changes: //! window_width: 300 //! box1.right: 50 //! box2.left: 200 //! box2.right: 300 //! ``` //! //! Note that the value of `box1.left` is not mentioned. This is because `solver.fetch_changes` only //! lists *changes* to variables, and since each variable starts in the solver with a value of zero, //! any values that have not changed from zero will not be reported. //! //! Now let's try compressing the window so that the boxes can't take up their preferred widths. //! //! ```ignore //! solver.suggest_value(window_width, 75.0); //! print_changes(&names, solver.fetch_changes); //! ``` //! //! Now the solver can't satisfy all of the constraints. It will pick at least one of the weakest //! constraints to violate. In this case it will be one or both of the preferred widths. For //! efficiency reasons this is picked nondeterministically, so there are two possible results. This //! could be //! //! ```ignore //! Changes: //! window_width: 75 //! box1.right: 0 //! box2.left: 0 //! box2.right: 75 //! ``` //! //! or //! //! ```ignore //! Changes: //! window_width: 75 //! box2.left: 50 //! box2.right: 75 //! ``` //! //! Due to the nature of the algorithm, "in-between" solutions, although just as valid, are not //! picked. //! //! In a user interface this is not likely a result we would prefer. The solution is to add another //! constraint to control the behaviour when the preferred widths cannot both be satisfied. In this //! example we are going to constrain the boxes to try to maintain a ratio between their widths. //! //! ``` //! # use kasuari::{ Solver, Variable, Strength }; //! # use kasuari::WeightedRelation::*; //! # //! # use hashbrown::HashMap; //! # let mut names = HashMap::new(); //! # fn print_changes(names: &HashMap, changes: &[(Variable, f64)]) { //! # println!("Changes:"); //! # for &(ref var, ref val) in changes { //! # println!("{}: {}", names[var], val); //! # } //! # } //! # //! # let window_width = Variable::new(); //! # names.insert(window_width, "window_width"); //! # struct Element { //! # left: Variable, //! # right: Variable //! # } //! # let box1 = Element { //! # left: Variable::new(), //! # right: Variable::new() //! # }; //! # names.insert(box1.left, "box1.left"); //! # names.insert(box1.right, "box1.right"); //! # let box2 = Element { //! # left: Variable::new(), //! # right: Variable::new() //! # }; //! # names.insert(box2.left, "box2.left"); //! # names.insert(box2.right, "box2.right"); //! # let mut solver = Solver::new(); //! # solver.add_constraints([ //! # window_width |GE(Strength::REQUIRED)| 0.0, // positive window width //! # box1.left |EQ(Strength::REQUIRED)| 0.0, // left align //! # box2.right |EQ(Strength::REQUIRED)| window_width, // right align //! # box2.left |GE(Strength::REQUIRED)| box1.right, // no overlap //! # // positive widths //! # box1.left |LE(Strength::REQUIRED)| box1.right, //! # box2.left |LE(Strength::REQUIRED)| box2.right, //! # // preferred widths: //! # box1.right - box1.left |EQ(Strength::WEAK)| 50.0, //! # box2.right - box2.left |EQ(Strength::WEAK)| 100.0]).unwrap(); //! # solver.add_edit_variable(window_width, Strength::STRONG).unwrap(); //! # solver.suggest_value(window_width, 300.0).unwrap(); //! # print_changes(&names, solver.fetch_changes()); //! # solver.suggest_value(window_width, 75.0); //! # print_changes(&names, solver.fetch_changes()); //! solver.add_constraint( //! (box1.right - box1.left) / 50.0 |EQ(Strength::MEDIUM)| (box2.right - box2.left) / 100.0 //! ).unwrap(); //! print_changes(&names, solver.fetch_changes()); //! ``` //! //! Now the result gives values that maintain the ratio between the sizes of the two boxes: //! //! ```ignore //! Changes: //! box1.right: 25 //! box2.left: 25 //! ``` //! //! This example may have appeared somewhat contrived, but hopefully it shows the power of the //! cassowary algorithm for laying out user interfaces. //! //! One thing that this example exposes is that this crate is a rather low level library. It does //! not have any inherent knowledge of user interfaces, directions or boxes. Thus for use in a user //! interface this crate should ideally be wrapped by a higher level API, which is outside the scope //! of this crate. #![cfg_attr(feature = "document-features", doc = "\n## Features")] #![cfg_attr(feature = "document-features", doc = document_features::document_features!())] #![no_std] extern crate alloc; mod constraint; mod error; mod expression; mod relations; mod row; mod solver; mod strength; mod term; mod variable; pub use self::constraint::{Constraint, PartialConstraint}; pub use self::error::{ AddConstraintError, AddEditVariableError, RemoveConstraintError, RemoveEditVariableError, SuggestValueError, }; pub use self::expression::Expression; pub use self::relations::{RelationalOperator, WeightedRelation}; pub use self::solver::{InternalSolverError, Solver}; pub use self::strength::Strength; pub use self::term::Term; pub use self::variable::Variable; kasuari-0.4.11/src/relations.rs000064400000000000000000000050051046102023000145030ustar 00000000000000use core::{fmt, ops}; use crate::{Expression, PartialConstraint, Strength, Term, Variable}; /// The possible relations that a constraint can specify. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum RelationalOperator { /// `<=` LessOrEqual, /// `==` Equal, /// `>=` GreaterOrEqual, } impl fmt::Display for RelationalOperator { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { match *self { RelationalOperator::LessOrEqual => write!(fmt, "<=")?, RelationalOperator::Equal => write!(fmt, "==")?, RelationalOperator::GreaterOrEqual => write!(fmt, ">=")?, }; Ok(()) } } /// This is part of the syntactic sugar used for specifying constraints. This enum should be used as /// part of a constraint expression. See the module documentation for more information. pub enum WeightedRelation { /// `==` EQ(Strength), /// `<=` LE(Strength), /// `>=` GE(Strength), } impl From for (RelationalOperator, Strength) { fn from(relation: WeightedRelation) -> (RelationalOperator, Strength) { match relation { WeightedRelation::EQ(s) => (RelationalOperator::Equal, s), WeightedRelation::LE(s) => (RelationalOperator::LessOrEqual, s), WeightedRelation::GE(s) => (RelationalOperator::GreaterOrEqual, s), } } } impl ops::BitOr for f64 { type Output = PartialConstraint; #[inline] fn bitor(self, rhs: WeightedRelation) -> PartialConstraint { PartialConstraint::new(Expression::from_constant(self), rhs) } } impl ops::BitOr for f32 { type Output = PartialConstraint; #[inline] fn bitor(self, rhs: WeightedRelation) -> PartialConstraint { (self as f64).bitor(rhs) } } impl ops::BitOr for Variable { type Output = PartialConstraint; #[inline] fn bitor(self, rhs: WeightedRelation) -> PartialConstraint { PartialConstraint::new(Expression::from_variable(self), rhs) } } impl ops::BitOr for Term { type Output = PartialConstraint; #[inline] fn bitor(self, rhs: WeightedRelation) -> PartialConstraint { PartialConstraint::new(Expression::from_term(self), rhs) } } impl ops::BitOr for Expression { type Output = PartialConstraint; #[inline] fn bitor(self, rhs: WeightedRelation) -> PartialConstraint { PartialConstraint::new(self, rhs) } } kasuari-0.4.11/src/row.rs000064400000000000000000000055551046102023000133240ustar 00000000000000use hashbrown::hash_map::Entry; use hashbrown::HashMap; #[derive(Debug, Clone)] pub struct Row { pub cells: HashMap, pub constant: f64, } #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Symbol(usize, SymbolKind); #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum SymbolKind { Invalid, External, Slack, Error, Dummy, } impl Symbol { pub fn new(id: usize, kind: SymbolKind) -> Symbol { Symbol(id, kind) } pub fn invalid() -> Symbol { Symbol(0, SymbolKind::Invalid) } pub fn kind(&self) -> SymbolKind { self.1 } } pub fn near_zero(value: f64) -> bool { const EPS: f64 = 1E-8; if value < 0.0 { -value < EPS } else { value < EPS } } impl Row { pub fn new(constant: f64) -> Row { Row { cells: HashMap::new(), constant, } } pub fn add(&mut self, v: f64) -> f64 { self.constant += v; self.constant } pub fn insert_symbol(&mut self, s: Symbol, coefficient: f64) { match self.cells.entry(s) { Entry::Vacant(entry) => { if !near_zero(coefficient) { entry.insert(coefficient); } } Entry::Occupied(mut entry) => { *entry.get_mut() += coefficient; if near_zero(*entry.get_mut()) { entry.remove(); } } } } pub fn insert_row(&mut self, other: &Row, coefficient: f64) -> bool { let constant_diff = other.constant * coefficient; self.constant += constant_diff; for (s, v) in &other.cells { self.insert_symbol(*s, v * coefficient); } constant_diff != 0.0 } pub fn remove(&mut self, s: Symbol) { self.cells.remove(&s); } pub fn reverse_sign(&mut self) { self.constant = -self.constant; for v in self.cells.values_mut() { *v = -*v; } } pub fn solve_for_symbol(&mut self, s: Symbol) { let coeff = -1.0 / match self.cells.entry(s) { Entry::Occupied(entry) => entry.remove(), Entry::Vacant(_) => unreachable!(), }; self.constant *= coeff; for v in self.cells.values_mut() { *v *= coeff; } } pub fn solve_for_symbols(&mut self, lhs: Symbol, rhs: Symbol) { self.insert_symbol(lhs, -1.0); self.solve_for_symbol(rhs); } pub fn coefficient_for(&self, s: Symbol) -> f64 { self.cells.get(&s).cloned().unwrap_or(0.0) } pub fn substitute(&mut self, s: Symbol, row: &Row) -> bool { if let Some(coeff) = self.cells.remove(&s) { self.insert_row(row, coeff) } else { false } } } kasuari-0.4.11/src/solver.rs000064400000000000000000000765271046102023000140360ustar 00000000000000use alloc::boxed::Box; use alloc::rc::Rc; use alloc::vec::Vec; use core::cell::RefCell; use core::f64; use hashbrown::hash_map::Entry; use hashbrown::{HashMap, HashSet}; use crate::constraint::Constraint; use crate::row::{near_zero, Row, Symbol, SymbolKind}; use crate::strength::Strength; use crate::{ AddConstraintError, AddEditVariableError, Expression, RelationalOperator, RemoveConstraintError, RemoveEditVariableError, SuggestValueError, Term, Variable, }; #[derive(Debug, Copy, Clone, thiserror::Error)] #[error("The solver entered an invalid state. If this occurs please report the issue.")] pub enum InternalSolverError { #[error("The objective is unbounded.")] ObjectiveUnbounded, #[error("Dual optimize failed.")] DualOptimizeFailed, #[error("Failed to find leaving row.")] FailedToFindLeavingRow, #[error("Edit constraint not in system")] EditConstraintNotInSystem, } #[derive(Copy, Clone)] struct Tag { marker: Symbol, other: Symbol, } #[derive(Clone)] struct EditInfo { tag: Tag, constraint: Constraint, constant: f64, } /// A constraint solver using the Cassowary algorithm. For proper usage please see the top level /// crate documentation. pub struct Solver { constraints: HashMap, var_data: HashMap, var_for_symbol: HashMap, public_changes: Vec<(Variable, f64)>, changed: HashSet, should_clear_changes: bool, rows: HashMap>, edits: HashMap, infeasible_rows: Vec, // never contains external symbols objective: Rc>, artificial: Option>>, id_tick: usize, } impl Default for Solver { fn default() -> Self { Self::new() } } impl Solver { /// Construct a new solver. pub fn new() -> Solver { Solver { constraints: HashMap::new(), var_data: HashMap::new(), var_for_symbol: HashMap::new(), public_changes: Vec::new(), changed: HashSet::new(), should_clear_changes: false, rows: HashMap::new(), edits: HashMap::new(), infeasible_rows: Vec::new(), objective: Rc::new(RefCell::new(Row::new(0.0))), artificial: None, id_tick: 1, } } pub fn add_constraints>( &mut self, constraints: I, ) -> Result<(), AddConstraintError> { for constraint in constraints { self.add_constraint(constraint)?; } Ok(()) } /// Add a constraint to the solver. pub fn add_constraint(&mut self, constraint: Constraint) -> Result<(), AddConstraintError> { if self.constraints.contains_key(&constraint) { // TODO detrmine if we could just ignore duplicate constraints return Err(AddConstraintError::DuplicateConstraint); } // Creating a row causes symbols to reserved for the variables in the constraint. If this // method exits with an exception, then its possible those variables will linger in the var // map. Since its likely that those variables will be used in other constraints and since // exceptional conditions are uncommon, i'm not too worried about aggressive cleanup of the // var map. let (mut row, tag) = self.create_row(&constraint); let mut subject = Solver::choose_subject(&row, &tag); // If choose_subject could find a valid entering symbol, one last option is available if the // entire row is composed of dummy variables. If the constant of the row is zero, then this // represents redundant constraints and the new dummy marker can enter the basis. If the // constant is non-zero, then it represents an unsatisfiable constraint. if subject.kind() == SymbolKind::Invalid && Solver::all_dummies(&row) { if !near_zero(row.constant) { return Err(AddConstraintError::UnsatisfiableConstraint); } else { subject = tag.marker; } } // If an entering symbol still isn't found, then the row must be added using an artificial // variable. If that fails, then the row represents an unsatisfiable constraint. if subject.kind() == SymbolKind::Invalid { let satisfiable = self.add_with_artificial_variable(&row)?; if !satisfiable { return Err(AddConstraintError::UnsatisfiableConstraint); } } else { row.solve_for_symbol(subject); self.substitute(subject, &row); if subject.kind() == SymbolKind::External && row.constant != 0.0 { let v = self.var_for_symbol[&subject]; self.var_changed(v); } self.rows.insert(subject, row); } self.constraints.insert(constraint, tag); // Optimizing after each constraint is added performs less aggregate work due to a smaller // average system size. It also ensures the solver remains in a consistent state. let objective = self.objective.clone(); self.optimize(&objective)?; Ok(()) } /// Remove a constraint from the solver. pub fn remove_constraint( &mut self, constraint: &Constraint, ) -> Result<(), RemoveConstraintError> { let tag = self .constraints .remove(constraint) .ok_or(RemoveConstraintError::UnknownConstraint)?; // Remove the error effects from the objective function // *before* pivoting, or substitutions into the objective // will lead to incorrect solver results. self.remove_constraint_effects(constraint, &tag); // If the marker is basic, simply drop the row. Otherwise, // pivot the marker into the basis and then drop the row. if self.rows.remove(&tag.marker).is_none() { let (leaving, mut row) = self.get_marker_leaving_row(tag.marker).ok_or( RemoveConstraintError::InternalSolverError( InternalSolverError::FailedToFindLeavingRow, ), )?; row.solve_for_symbols(leaving, tag.marker); self.substitute(tag.marker, &row); } // Optimizing after each constraint is removed ensures that the // solver remains consistent. It makes the solver api easier to // use at a small tradeoff for speed. let objective = self.objective.clone(); self.optimize(&objective)?; // Check for and decrease the reference count for variables referenced by the constraint // If the reference count is zero remove the variable from the variable map for term in &constraint.expr().terms { if !near_zero(term.coefficient) { let mut should_remove = false; if let Some(&mut (_, _, ref mut count)) = self.var_data.get_mut(&term.variable) { *count -= 1; should_remove = *count == 0; } if should_remove { self.var_for_symbol.remove(&self.var_data[&term.variable].1); self.var_data.remove(&term.variable); } } } Ok(()) } /// Test whether a constraint has been added to the solver. pub fn has_constraint(&self, constraint: &Constraint) -> bool { self.constraints.contains_key(constraint) } /// Add an edit variable to the solver. /// /// This method should be called before the `suggest_value` method is /// used to supply a suggested value for the given edit variable. pub fn add_edit_variable( &mut self, v: Variable, strength: Strength, ) -> Result<(), AddEditVariableError> { if self.edits.contains_key(&v) { return Err(AddEditVariableError::DuplicateEditVariable); } if strength == Strength::REQUIRED { return Err(AddEditVariableError::BadRequiredStrength); } let cn = Constraint::new( Expression::from_term(Term::new(v, 1.0)), RelationalOperator::Equal, strength, ); self.add_constraint(cn.clone()).unwrap(); self.edits.insert( v, EditInfo { tag: self.constraints[&cn], constraint: cn, constant: 0.0, }, ); Ok(()) } /// Remove an edit variable from the solver. pub fn remove_edit_variable(&mut self, v: Variable) -> Result<(), RemoveEditVariableError> { if let Some(constraint) = self.edits.remove(&v).map(|e| e.constraint) { self.remove_constraint(&constraint).map_err(|e| match e { RemoveConstraintError::UnknownConstraint => { RemoveEditVariableError::InternalSolverError( InternalSolverError::EditConstraintNotInSystem, ) } RemoveConstraintError::InternalSolverError(s) => { RemoveEditVariableError::InternalSolverError(s) } })?; Ok(()) } else { Err(RemoveEditVariableError::UnknownEditVariable) } } /// Test whether an edit variable has been added to the solver. pub fn has_edit_variable(&self, v: &Variable) -> bool { self.edits.contains_key(v) } /// Suggest a value for the given edit variable. /// /// This method should be used after an edit variable has been added to /// the solver in order to suggest the value for that variable. pub fn suggest_value( &mut self, variable: Variable, value: f64, ) -> Result<(), SuggestValueError> { let (info_tag_marker, info_tag_other, delta) = { let info = self .edits .get_mut(&variable) .ok_or(SuggestValueError::UnknownEditVariable)?; let delta = value - info.constant; info.constant = value; (info.tag.marker, info.tag.other, delta) }; // tag.marker and tag.other are never external symbols // The nice version of the following code runs into non-lexical borrow issues. // Ideally the `if row...` code would be in the body of the if. Pretend that it is. { let infeasible_rows = &mut self.infeasible_rows; if self .rows .get_mut(&info_tag_marker) .map(|row| { if row.add(-delta) < 0.0 { infeasible_rows.push(info_tag_marker); } }) .is_some() || self .rows .get_mut(&info_tag_other) .map(|row| { if row.add(delta) < 0.0 { infeasible_rows.push(info_tag_other); } }) .is_some() { } else { for (symbol, row) in &mut self.rows { let coeff = row.coefficient_for(info_tag_marker); let diff = delta * coeff; if diff != 0.0 && symbol.kind() == SymbolKind::External { let v = self.var_for_symbol[symbol]; // inline var_changed - borrow checker workaround if self.should_clear_changes { self.changed.clear(); self.should_clear_changes = false; } self.changed.insert(v); } if coeff != 0.0 && row.add(diff) < 0.0 && symbol.kind() != SymbolKind::External { infeasible_rows.push(*symbol); } } } } self.dual_optimize()?; Ok(()) } fn var_changed(&mut self, v: Variable) { if self.should_clear_changes { self.changed.clear(); self.should_clear_changes = false; } self.changed.insert(v); } /// Fetches all changes to the values of variables since the last call to this function. /// /// The list of changes returned is not in a specific order. Each change comprises the variable /// changed and the new value of that variable. pub fn fetch_changes(&mut self) -> &[(Variable, f64)] { if self.should_clear_changes { self.changed.clear(); self.should_clear_changes = false; } else { self.should_clear_changes = true; } self.public_changes.clear(); for &v in &self.changed { if let Some(var_data) = self.var_data.get_mut(&v) { let new_value = self .rows .get(&var_data.1) .map(|r| r.constant) .unwrap_or(0.0); let old_value = var_data.0; if old_value != new_value { self.public_changes.push((v, new_value)); var_data.0 = new_value; } } } &self.public_changes } /// Reset the solver to the empty starting condition. /// /// This method resets the internal solver state to the empty starting /// condition, as if no constraints or edit variables have been added. /// This can be faster than deleting the solver and creating a new one /// when the entire system must change, since it can avoid unnecessary /// heap (de)allocations. pub fn reset(&mut self) { self.rows.clear(); self.constraints.clear(); self.var_data.clear(); self.var_for_symbol.clear(); self.changed.clear(); self.should_clear_changes = false; self.edits.clear(); self.infeasible_rows.clear(); *self.objective.borrow_mut() = Row::new(0.0); self.artificial = None; self.id_tick = 1; } /// Get the symbol for the given variable. /// /// If a symbol does not exist for the variable, one will be created. fn get_var_symbol(&mut self, v: Variable) -> Symbol { let id_tick = &mut self.id_tick; let var_for_symbol = &mut self.var_for_symbol; let value = self.var_data.entry(v).or_insert_with(|| { let s = Symbol::new(*id_tick, SymbolKind::External); var_for_symbol.insert(s, v); *id_tick += 1; (f64::NAN, s, 0) }); value.2 += 1; value.1 } /// Create a new Row object for the given constraint. /// /// The terms in the constraint will be converted to cells in the row. Any term in the /// constraint with a coefficient of zero is ignored. This method uses the `get_var_symbol` /// method to get the symbol for the variables added to the row. If the symbol for a given cell /// variable is basic, the cell variable will be substituted with the basic row. /// /// The necessary slack and error variables will be added to the row. If the constant for the /// row is negative, the sign for the row will be inverted so the constant becomes positive. /// /// The tag will be updated with the marker and error symbols to use for tracking the movement /// of the constraint in the tableau. fn create_row(&mut self, constraint: &Constraint) -> (Box, Tag) { let expr = constraint.expr(); let mut row = Row::new(expr.constant); // Substitute the current basic variables into the row. for term in &expr.terms { if !near_zero(term.coefficient) { let symbol = self.get_var_symbol(term.variable); if let Some(other_row) = self.rows.get(&symbol) { row.insert_row(other_row, term.coefficient); } else { row.insert_symbol(symbol, term.coefficient); } } } let mut objective = self.objective.borrow_mut(); // Add the necessary slack, error, and dummy variables. let tag = match constraint.op() { RelationalOperator::GreaterOrEqual | RelationalOperator::LessOrEqual => { let coeff = if constraint.op() == RelationalOperator::LessOrEqual { 1.0 } else { -1.0 }; let slack = Symbol::new(self.id_tick, SymbolKind::Slack); self.id_tick += 1; row.insert_symbol(slack, coeff); if constraint.strength() < Strength::REQUIRED { let error = Symbol::new(self.id_tick, SymbolKind::Error); self.id_tick += 1; row.insert_symbol(error, -coeff); objective.insert_symbol(error, constraint.strength().value()); Tag { marker: slack, other: error, } } else { Tag { marker: slack, other: Symbol::invalid(), } } } RelationalOperator::Equal => { if constraint.strength() < Strength::REQUIRED { let errplus = Symbol::new(self.id_tick, SymbolKind::Error); self.id_tick += 1; let errminus = Symbol::new(self.id_tick, SymbolKind::Error); self.id_tick += 1; row.insert_symbol(errplus, -1.0); // v = eplus - eminus row.insert_symbol(errminus, 1.0); // v - eplus + eminus = 0 objective.insert_symbol(errplus, constraint.strength().value()); objective.insert_symbol(errminus, constraint.strength().value()); Tag { marker: errplus, other: errminus, } } else { let dummy = Symbol::new(self.id_tick, SymbolKind::Dummy); self.id_tick += 1; row.insert_symbol(dummy, 1.0); Tag { marker: dummy, other: Symbol::invalid(), } } } }; // Ensure the row has a positive constant. if row.constant < 0.0 { row.reverse_sign(); } (Box::new(row), tag) } /// Choose the subject for solving for the row. /// /// This method will choose the best subject for using as the solve /// target for the row. An invalid symbol will be returned if there /// is no valid target. /// /// The symbols are chosen according to the following precedence: /// /// 1) The first symbol representing an external variable. /// 2) A negative slack or error tag variable. /// /// If a subject cannot be found, an invalid symbol will be returned. fn choose_subject(row: &Row, tag: &Tag) -> Symbol { for s in row.cells.keys() { if s.kind() == SymbolKind::External { return *s; } } if (tag.marker.kind() == SymbolKind::Slack || tag.marker.kind() == SymbolKind::Error) && row.coefficient_for(tag.marker) < 0.0 { return tag.marker; } if (tag.other.kind() == SymbolKind::Slack || tag.other.kind() == SymbolKind::Error) && row.coefficient_for(tag.other) < 0.0 { return tag.other; } Symbol::invalid() } /// Add the row to the tableau using an artificial variable. /// /// This will return false if the constraint cannot be satisfied. fn add_with_artificial_variable(&mut self, row: &Row) -> Result { // Create and add the artificial variable to the tableau let art = Symbol::new(self.id_tick, SymbolKind::Slack); self.id_tick += 1; self.rows.insert(art, Box::new(row.clone())); self.artificial = Some(Rc::new(RefCell::new(row.clone()))); // Optimize the artificial objective. This is successful // only if the artificial objective is optimized to zero. let artificial = self.artificial.as_ref().unwrap().clone(); self.optimize(&artificial)?; let success = near_zero(artificial.borrow().constant); self.artificial = None; // If the artificial variable is basic, pivot the row so that // it becomes basic. If the row is constant, exit early. if let Some(mut row) = self.rows.remove(&art) { if row.cells.is_empty() { return Ok(success); } let entering = Solver::any_pivotable_symbol(&row); // never External if entering.kind() == SymbolKind::Invalid { return Ok(false); // unsatisfiable (will this ever happen?) } row.solve_for_symbols(art, entering); self.substitute(entering, &row); self.rows.insert(entering, row); } // Remove the artificial row from the tableau for row in self.rows.values_mut() { row.remove(art); } self.objective.borrow_mut().remove(art); Ok(success) } /// Substitute the parametric symbol with the given row. /// /// This method will substitute all instances of the parametric symbol /// in the tableau and the objective function with the given row. fn substitute(&mut self, symbol: Symbol, row: &Row) { for (&other_symbol, other_row) in &mut self.rows { let constant_changed = other_row.substitute(symbol, row); if other_symbol.kind() == SymbolKind::External && constant_changed { let v = self.var_for_symbol[&other_symbol]; // inline var_changed if self.should_clear_changes { self.changed.clear(); self.should_clear_changes = false; } self.changed.insert(v); } if other_symbol.kind() != SymbolKind::External && other_row.constant < 0.0 { self.infeasible_rows.push(other_symbol); } } self.objective.borrow_mut().substitute(symbol, row); if let Some(artificial) = self.artificial.as_ref() { artificial.borrow_mut().substitute(symbol, row); } } /// Optimize the system for the given objective function. /// /// This method performs iterations of Phase 2 of the simplex method /// until the objective function reaches a minimum. fn optimize(&mut self, objective: &RefCell) -> Result<(), InternalSolverError> { loop { let entering = Solver::get_entering_symbol(&objective.borrow()); if entering.kind() == SymbolKind::Invalid { return Ok(()); } let (leaving, mut row) = self .get_leaving_row(entering) .ok_or(InternalSolverError::ObjectiveUnbounded)?; // pivot the entering symbol into the basis row.solve_for_symbols(leaving, entering); self.substitute(entering, &row); if entering.kind() == SymbolKind::External && row.constant != 0.0 { let v = self.var_for_symbol[&entering]; self.var_changed(v); } self.rows.insert(entering, row); } } /// Optimize the system using the dual of the simplex method. /// /// The current state of the system should be such that the objective /// function is optimal, but not feasible. This method will perform /// an iteration of the dual simplex method to make the solution both /// optimal and feasible. fn dual_optimize(&mut self) -> Result<(), InternalSolverError> { while let Some(leaving) = self.infeasible_rows.pop() { let row = if let Entry::Occupied(entry) = self.rows.entry(leaving) { if entry.get().constant < 0.0 { Some(entry.remove()) } else { None } } else { None }; if let Some(mut row) = row { let entering = self.get_dual_entering_symbol(&row); if entering.kind() == SymbolKind::Invalid { return Err(InternalSolverError::DualOptimizeFailed); } // pivot the entering symbol into the basis row.solve_for_symbols(leaving, entering); self.substitute(entering, &row); if entering.kind() == SymbolKind::External && row.constant != 0.0 { let v = self.var_for_symbol[&entering]; self.var_changed(v); } self.rows.insert(entering, row); } } Ok(()) } /// Compute the entering variable for a pivot operation. /// /// This method will return first symbol in the objective function which /// is non-dummy and has a coefficient less than zero. If no symbol meets /// the criteria, it means the objective function is at a minimum, and an /// invalid symbol is returned. /// Could return an External symbol fn get_entering_symbol(objective: &Row) -> Symbol { for (symbol, value) in &objective.cells { if symbol.kind() != SymbolKind::Dummy && *value < 0.0 { return *symbol; } } Symbol::invalid() } /// Compute the entering symbol for the dual optimize operation. /// /// This method will return the symbol in the row which has a positive /// coefficient and yields the minimum ratio for its respective symbol /// in the objective function. The provided row *must* be infeasible. /// If no symbol is found which meats the criteria, an invalid symbol /// is returned. /// Could return an External symbol fn get_dual_entering_symbol(&self, row: &Row) -> Symbol { let mut entering = Symbol::invalid(); let mut ratio = f64::INFINITY; let objective = self.objective.borrow(); for (symbol, value) in &row.cells { if *value > 0.0 && symbol.kind() != SymbolKind::Dummy { let coeff = objective.coefficient_for(*symbol); let r = coeff / *value; if r < ratio { ratio = r; entering = *symbol; } } } entering } /// Get the first Slack or Error symbol in the row. /// /// If no such symbol is present, and Invalid symbol will be returned. /// Never returns an External symbol fn any_pivotable_symbol(row: &Row) -> Symbol { for symbol in row.cells.keys() { if symbol.kind() == SymbolKind::Slack || symbol.kind() == SymbolKind::Error { return *symbol; } } Symbol::invalid() } /// Compute the row which holds the exit symbol for a pivot. /// /// This method will return an iterator to the row in the row map /// which holds the exit symbol. If no appropriate exit symbol is /// found, the end() iterator will be returned. This indicates that /// the objective function is unbounded. /// Never returns a row for an External symbol fn get_leaving_row(&mut self, entering: Symbol) -> Option<(Symbol, Box)> { let mut ratio = f64::INFINITY; let mut found = None; for (symbol, row) in &self.rows { if symbol.kind() != SymbolKind::External { let temp = row.coefficient_for(entering); if temp < 0.0 { let temp_ratio = -row.constant / temp; if temp_ratio < ratio { ratio = temp_ratio; found = Some(*symbol); } } } } found.map(|s| (s, self.rows.remove(&s).unwrap())) } /// Compute the leaving row for a marker variable. /// /// This method will return an iterator to the row in the row map /// which holds the given marker variable. The row will be chosen /// according to the following precedence: /// /// 1) The row with a restricted basic varible and a negative coefficient for the marker with /// the smallest ratio of -constant / coefficient. /// /// 2) The row with a restricted basic variable and the smallest ratio of constant / /// coefficient. /// /// 3) The last unrestricted row which contains the marker. /// /// If the marker does not exist in any row, the row map end() iterator /// will be returned. This indicates an internal solver error since /// the marker *should* exist somewhere in the tableau. fn get_marker_leaving_row(&mut self, marker: Symbol) -> Option<(Symbol, Box)> { let mut r1 = f64::INFINITY; let mut r2 = r1; let mut first = None; let mut second = None; let mut third = None; for (symbol, row) in &self.rows { let c = row.coefficient_for(marker); if c == 0.0 { continue; } if symbol.kind() == SymbolKind::External { third = Some(*symbol); } else if c < 0.0 { let r = -row.constant / c; if r < r1 { r1 = r; first = Some(*symbol); } } else { let r = row.constant / c; if r < r2 { r2 = r; second = Some(*symbol); } } } first.or(second).or(third).and_then(|s| { if s.kind() == SymbolKind::External && self.rows[&s].constant != 0.0 { let v = self.var_for_symbol[&s]; self.var_changed(v); } self.rows.remove(&s).map(|r| (s, r)) }) } /// Remove the effects of a constraint on the objective function. fn remove_constraint_effects(&mut self, constraint: &Constraint, tag: &Tag) { if tag.marker.kind() == SymbolKind::Error { self.remove_marker_effects(tag.marker, constraint.strength().value()); } if tag.other.kind() == SymbolKind::Error { self.remove_marker_effects(tag.other, constraint.strength().value()); } } /// Remove the effects of an error marker on the objective function. fn remove_marker_effects(&mut self, marker: Symbol, strength: f64) { if let Some(row) = self.rows.get(&marker) { self.objective.borrow_mut().insert_row(row, -strength); } else { self.objective.borrow_mut().insert_symbol(marker, -strength); } } /// Test whether a row is composed of all dummy variables. fn all_dummies(row: &Row) -> bool { for symbol in row.cells.keys() { if symbol.kind() != SymbolKind::Dummy { return false; } } true } /// Get the stored value for a variable. /// /// Normally values should be retrieved and updated using `fetch_changes`, but this method can /// be used for debugging or testing. pub fn get_value(&self, v: Variable) -> f64 { self.var_data .get(&v) .and_then(|s| self.rows.get(&s.1).map(|r| r.constant)) .unwrap_or(0.0) } } kasuari-0.4.11/src/strength.rs000064400000000000000000000313701046102023000143450ustar 00000000000000//! Contains useful constants and functions for producing strengths for use in the constraint //! solver. Each constraint added to the solver has an associated strength specifying the precedence //! the solver should impose when choosing which constraints to enforce. It will try to enforce all //! constraints, but if that is impossible the lowest strength constraints are the first to be //! violated. //! //! Strengths are simply real numbers. The strongest legal strength is 1,001,001,000.0. The weakest //! is 0.0. For convenience constants are declared for commonly used strengths. These are //! [`REQUIRED`], [`STRONG`], [`MEDIUM`] and [`WEAK`]. Feel free to multiply these by other values //! to get intermediate strengths. Note that the solver will clip given strengths to the legal //! range. //! //! [`REQUIRED`] signifies a constraint that cannot be violated under any circumstance. Use this //! special strength sparingly, as the solver will fail completely if it find that not all of the //! [`REQUIRED`] constraints can be satisfied. The other strengths represent fallible constraints. //! These should be the most commonly used strenghts for use cases where violating a constraint is //! acceptable or even desired. //! //! The solver will try to get as close to satisfying the constraints it violates as possible, //! strongest first. This behaviour can be used (for example) to provide a "default" value for a //! variable should no other stronger constraints be put upon it. use core::ops; #[derive(Debug, Copy, Clone, PartialEq)] pub struct Strength(f64); impl Strength { /// The required strength for a constraint. This is the strongest possible strength. pub const REQUIRED: Strength = Strength(1_001_001_000.0); /// A strong strength for a constraint. This is weaker than `REQUIRED` but stronger than /// `MEDIUM`. pub const STRONG: Strength = Strength(1_000_000.0); /// A medium strength for a constraint. This is weaker than `STRONG` but stronger than `WEAK`. pub const MEDIUM: Strength = Strength(1_000.0); /// A weak strength for a constraint. This is weaker than `MEDIUM` but stronger than `0.0`. pub const WEAK: Strength = Strength(1.0); /// The weakest possible strength for a constraint. This is weaker than `WEAK`. pub const ZERO: Strength = Strength(0.0); /// Create a new strength with the given value, clipped to the legal range (0.0, REQUIRED) #[inline] pub const fn new(value: f64) -> Self { Self(value.clamp(0.0, Self::REQUIRED.value())) } /// Create a constraint as a linear combination of STRONG, MEDIUM and WEAK strengths. /// /// Each weight is multiplied by the multiplier, clamped to the legal range and then multiplied /// by the corresponding strength. The resulting strengths are then summed. #[inline] pub const fn create(strong: f64, medium: f64, weak: f64, multiplier: f64) -> Self { let strong = (strong * multiplier).clamp(0.0, 1000.0) * Self::STRONG.value(); let medium = (medium * multiplier).clamp(0.0, 1000.0) * Self::MEDIUM.value(); let weak = (weak * multiplier).clamp(0.0, 1000.0) * Self::WEAK.value(); Self::new(strong + medium + weak) } /// The value of the strength #[inline] pub const fn value(&self) -> f64 { self.0 } /// Add two strengths together, clamping the result to the legal range #[inline] pub const fn add(self, rhs: Self) -> Self { Self::new(self.0 + rhs.0) } /// Subtract one strength from another, clipping the result to the legal range #[inline] pub const fn sub(self, rhs: Self) -> Self { Self::new(self.0 - rhs.0) } /// Multiply a strength by a scalar, clipping the result to the legal range #[inline] pub const fn mul_f64(self, rhs: f64) -> Self { Self::new(self.0 * rhs) } /// Multiply a strength by a scalar, clipping the result to the legal range #[inline] pub const fn mul_f32(self, rhs: f32) -> Self { Self::new(self.0 * rhs as f64) } /// Divide a strength by a scalar, clipping the result to the legal range #[inline] pub const fn div_f64(self, rhs: f64) -> Self { Self::new(self.0 / rhs) } /// Divide a strength by a scalar, clipping the result to the legal range #[inline] pub const fn div_f32(self, rhs: f32) -> Self { Self::new(self.0 / rhs as f64) } } impl ops::Add for Strength { type Output = Self; /// Add two strengths together, clipping the result to the legal range #[inline] fn add(self, rhs: Self) -> Self { Self::add(self, rhs) } } impl ops::Sub for Strength { type Output = Strength; /// Subtract one strength from another, clipping the result to the legal range #[inline] fn sub(self, rhs: Strength) -> Strength { Self::sub(self, rhs) } } impl ops::AddAssign for Strength { /// Perform an in-place addition of two strengths, clipping the result to the legal range #[inline] fn add_assign(&mut self, rhs: Self) { *self = *self + rhs; } } impl ops::SubAssign for Strength { /// Perform an in-place subtraction of two strengths, clipping the result to the legal range #[inline] fn sub_assign(&mut self, rhs: Self) { *self = *self - rhs; } } impl ops::Mul for Strength { type Output = Strength; /// Multiply a strength by a scalar, clipping the result to the legal range #[inline] fn mul(self, rhs: f64) -> Strength { self.mul_f64(rhs) } } impl ops::Mul for f64 { type Output = Strength; /// Multiply a scalar by a strength, clipping the result to the legal range #[inline] fn mul(self, rhs: Strength) -> Strength { rhs.mul_f64(self) } } impl ops::MulAssign for Strength { /// Perform an in-place multiplication of a strength by a scalar, clipping the result to the /// legal range #[inline] fn mul_assign(&mut self, rhs: f64) { *self = *self * rhs; } } impl core::cmp::Ord for Strength { #[inline] fn cmp(&self, other: &Self) -> core::cmp::Ordering { self.0.partial_cmp(&other.0).unwrap() } } impl core::cmp::PartialOrd for Strength { #[inline] fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl core::cmp::Eq for Strength {} #[cfg(test)] mod tests { use rstest::rstest; use super::*; #[rstest] #[case::under(-1.0, Strength::ZERO)] #[case::min(0.0, Strength::ZERO)] #[case::weak(1.0, Strength::WEAK)] #[case::medium(1_000.0, Strength::MEDIUM)] #[case::strong(1_000_000.0, Strength::STRONG)] #[case::required(1_001_001_000.0, Strength::REQUIRED)] #[case::over(1_001_001_001.0, Strength::REQUIRED)] fn new(#[case] value: f64, #[case] expected: Strength) { let strength = Strength::new(value); assert_eq!(strength, expected); } #[rstest] #[case::all_zeroes(0.0, 0.0, 0.0, 1.0, Strength::ZERO)] #[case::weak(0.0, 0.0, 1.0, 1.0, Strength::WEAK)] #[case::medium(0.0, 1.0, 0.0, 1.0, Strength::MEDIUM)] #[case::strong(1.0, 0.0, 0.0, 1.0, Strength::STRONG)] #[case::weak_clip(0.0, 0.0, 1000.0, 2.0, Strength::MEDIUM)] #[case::medium_clip(0.0, 1000.0, 0.0, 2.0, Strength::STRONG)] #[case::strong_clip(1000.0, 0.0, 0.0, 2.0, 1000.0 * Strength::STRONG)] #[case::all_non_zero(1.0, 1.0, 1.0, 1.0, Strength::STRONG + Strength::MEDIUM + Strength::WEAK)] #[case::multiplier(1.0, 1.0, 1.0, 2.0, 2.0 * (Strength::STRONG + Strength::MEDIUM + Strength::WEAK))] #[case::max(1000.0, 1000.0, 1000.0, 1.0, Strength::REQUIRED)] fn create( #[case] strong: f64, #[case] medium: f64, #[case] weak: f64, #[case] multiplier: f64, #[case] expected: Strength, ) { let strength = Strength::create(strong, medium, weak, multiplier); assert_eq!(strength, expected); } #[rstest] #[case::zero_plus_zero(Strength::ZERO, Strength::ZERO, Strength::ZERO)] #[case::zero_plus_weak(Strength::ZERO, Strength::WEAK, Strength::WEAK)] #[case::weak_plus_zero(Strength::WEAK, Strength::ZERO, Strength::WEAK)] #[case::weak_plus_weak(Strength::WEAK, Strength::WEAK, Strength::new(2.0))] #[case::weak_plus_medium(Strength::WEAK, Strength::MEDIUM, Strength::new(1001.0))] #[case::medium_plus_strong(Strength::MEDIUM, Strength::STRONG, Strength::new(1_001_000.0))] #[case::strong_plus_required(Strength::STRONG, Strength::REQUIRED, Strength::REQUIRED)] fn add(#[case] lhs: Strength, #[case] rhs: Strength, #[case] expected: Strength) { let result = lhs + rhs; assert_eq!(result, expected); } #[rstest] #[case::zero_plus_zero(Strength::ZERO, Strength::ZERO, Strength::ZERO)] #[case::zero_plus_weak(Strength::ZERO, Strength::WEAK, Strength::WEAK)] #[case::weak_plus_zero(Strength::WEAK, Strength::ZERO, Strength::WEAK)] #[case::weak_plus_weak(Strength::WEAK, Strength::WEAK, Strength::new(2.0))] #[case::weak_plus_medium(Strength::WEAK, Strength::MEDIUM, Strength::new(1001.0))] #[case::medium_plus_strong(Strength::MEDIUM, Strength::STRONG, Strength::new(1_001_000.0))] #[case::saturate_high(Strength::STRONG, Strength::REQUIRED, Strength::REQUIRED)] fn add_assign(#[case] lhs: Strength, #[case] rhs: Strength, #[case] expected: Strength) { let mut result = lhs; result += rhs; assert_eq!(result, expected); } #[rstest] #[case::saturate_low(Strength::ZERO, Strength::WEAK, Strength::ZERO)] #[case::zero_minus_zero(Strength::ZERO, Strength::ZERO, Strength::ZERO)] #[case::weak_minus_zero(Strength::WEAK, Strength::ZERO, Strength::WEAK)] #[case::weak_minus_weak(Strength::WEAK, Strength::WEAK, Strength::ZERO)] #[case::medium_minus_weak(Strength::MEDIUM, Strength::WEAK, Strength::new(999.0))] #[case::strong_minus_medium(Strength::STRONG, Strength::MEDIUM, Strength::new(999_000.0))] #[case::required_minus_strong( Strength::REQUIRED, Strength::STRONG, Strength::new(1_000_001_000.0) )] #[case::required_minus_required(Strength::REQUIRED, Strength::REQUIRED, Strength::ZERO)] fn sub(#[case] lhs: Strength, #[case] rhs: Strength, #[case] expected: Strength) { let result = lhs - rhs; assert_eq!(result, expected); } #[rstest] #[case::saturate_low(Strength::ZERO, Strength::WEAK, Strength::ZERO)] #[case::zero_minus_zero(Strength::ZERO, Strength::ZERO, Strength::ZERO)] #[case::weak_minus_zero(Strength::WEAK, Strength::ZERO, Strength::WEAK)] #[case::weak_minus_weak(Strength::WEAK, Strength::WEAK, Strength::ZERO)] #[case::medium_minus_weak(Strength::MEDIUM, Strength::WEAK, Strength::new(999.0))] #[case::strong_minus_medium(Strength::STRONG, Strength::MEDIUM, Strength::new(999_000.0))] #[case::required_minus_strong( Strength::REQUIRED, Strength::STRONG, Strength::new(1_000_001_000.0) )] #[case::required_minus_required(Strength::REQUIRED, Strength::REQUIRED, Strength::ZERO)] fn sub_assign(#[case] lhs: Strength, #[case] rhs: Strength, #[case] expected: Strength) { let mut result = lhs; result -= rhs; assert_eq!(result, expected); } #[rstest] #[case::negative(Strength::WEAK, -1.0, Strength::ZERO)] #[case::zero_mul_zero(Strength::ZERO, 0.0, Strength::ZERO)] #[case::zero_mul_one(Strength::ZERO, 1.0, Strength::ZERO)] #[case::weak_mul_zero(Strength::WEAK, 0.0, Strength::ZERO)] #[case::weak_mul_one(Strength::WEAK, 1.0, Strength::WEAK)] #[case::weak_mul_two(Strength::WEAK, 2.0, Strength::new(2.0))] #[case::medium_mul_half(Strength::MEDIUM, 0.5, Strength::new(500.0))] #[case::strong_mul_two(Strength::STRONG, 2.0, Strength::new(2_000_000.0))] #[case::required_mul_half(Strength::REQUIRED, 0.5, Strength::new(500_500_500.0))] fn mul(#[case] lhs: Strength, #[case] rhs: f64, #[case] expected: Strength) { let result = lhs * rhs; assert_eq!(result, expected); } #[rstest] #[case::negative(Strength::WEAK, -1.0, Strength::ZERO)] #[case::zero_mul_zero(Strength::ZERO, 0.0, Strength::ZERO)] #[case::zero_mul_one(Strength::ZERO, 1.0, Strength::ZERO)] #[case::weak_mul_zero(Strength::WEAK, 0.0, Strength::ZERO)] #[case::weak_mul_one(Strength::WEAK, 1.0, Strength::WEAK)] #[case::weak_mul_two(Strength::WEAK, 2.0, Strength::new(2.0))] #[case::medium_mul_half(Strength::MEDIUM, 0.5, Strength::new(500.0))] #[case::strong_mul_two(Strength::STRONG, 2.0, Strength::new(2_000_000.0))] #[case::required_mul_half(Strength::REQUIRED, 0.5, Strength::new(500_500_500.0))] fn mul_assign(#[case] lhs: Strength, #[case] rhs: f64, #[case] expected: Strength) { let mut result = lhs; result *= rhs; assert_eq!(result, expected); } } kasuari-0.4.11/src/term.rs000064400000000000000000000233431046102023000134570ustar 00000000000000use alloc::vec; use core::ops; use crate::{Expression, Variable}; /// A variable and a coefficient to multiply that variable by. /// /// This is a sub-expression in a constraint equation that represents: /// /// ```text /// term = coefficient * variable /// ``` #[derive(Copy, Clone, Debug, PartialEq)] pub struct Term { pub variable: Variable, pub coefficient: f64, } impl Term { /// Construct a new Term from a variable and a coefficient. #[inline] pub const fn new(variable: Variable, coefficient: f64) -> Term { Term { variable, coefficient, } } /// Construct a new Term from a variable with a coefficient of 1.0. #[inline] pub const fn from_variable(variable: Variable) -> Term { Term::new(variable, 1.0) } } impl From for Term { #[inline] fn from(variable: Variable) -> Term { Term::from_variable(variable) } } impl ops::Mul for Term { type Output = Term; #[inline] fn mul(self, rhs: f64) -> Term { Term::new(self.variable, self.coefficient * rhs) } } impl ops::Mul for f64 { type Output = Term; #[inline] fn mul(self, rhs: Term) -> Term { Term::new(rhs.variable, self * rhs.coefficient) } } impl ops::Mul for Term { type Output = Term; #[inline] fn mul(self, rhs: f32) -> Term { Term::new(self.variable, self.coefficient * rhs as f64) } } impl ops::Mul for f32 { type Output = Term; #[inline] fn mul(self, rhs: Term) -> Term { Term::new(rhs.variable, self as f64 * rhs.coefficient) } } impl ops::MulAssign for Term { #[inline] fn mul_assign(&mut self, rhs: f64) { self.coefficient *= rhs; } } impl ops::MulAssign for Term { #[inline] fn mul_assign(&mut self, rhs: f32) { self.coefficient *= rhs as f64; } } impl ops::Div for Term { type Output = Term; #[inline] fn div(self, rhs: f64) -> Term { Term::new(self.variable, self.coefficient / rhs) } } impl ops::Div for Term { type Output = Term; #[inline] fn div(self, rhs: f32) -> Term { Term::new(self.variable, self.coefficient / rhs as f64) } } impl ops::DivAssign for Term { #[inline] fn div_assign(&mut self, rhs: f64) { self.coefficient /= rhs; } } impl ops::DivAssign for Term { #[inline] fn div_assign(&mut self, rhs: f32) { self.coefficient /= rhs as f64; } } impl ops::Add for Term { type Output = Expression; #[inline] fn add(self, rhs: f64) -> Expression { Expression::new(vec![self], rhs) } } impl ops::Add for Term { type Output = Expression; #[inline] fn add(self, rhs: f32) -> Expression { Expression::new(vec![self], rhs as f64) } } impl ops::Add for f64 { type Output = Expression; #[inline] fn add(self, rhs: Term) -> Expression { Expression::new(vec![rhs], self) } } impl ops::Add for f32 { type Output = Expression; #[inline] fn add(self, rhs: Term) -> Expression { Expression::new(vec![rhs], self as f64) } } impl ops::Add for Term { type Output = Expression; #[inline] fn add(self, rhs: Term) -> Expression { Expression::from_terms(vec![self, rhs]) } } impl ops::Add for Term { type Output = Expression; #[inline] fn add(self, mut rhs: Expression) -> Expression { rhs.terms.insert(0, self); rhs } } impl ops::Add for Expression { type Output = Expression; #[inline] fn add(mut self, rhs: Term) -> Expression { self.terms.push(rhs); self } } impl ops::AddAssign for Expression { #[inline] fn add_assign(&mut self, rhs: Term) { self.terms.push(rhs); } } impl ops::Neg for Term { type Output = Term; #[inline] fn neg(mut self) -> Term { self.coefficient = -self.coefficient; self } } impl ops::Sub for Term { type Output = Expression; #[inline] fn sub(self, rhs: f64) -> Expression { Expression::new(vec![self], -rhs) } } impl ops::Sub for Term { type Output = Expression; #[inline] fn sub(self, rhs: f32) -> Expression { Expression::new(vec![self], -(rhs as f64)) } } impl ops::Sub for f64 { type Output = Expression; #[inline] fn sub(self, rhs: Term) -> Expression { Expression::new(vec![-rhs], self) } } impl ops::Sub for f32 { type Output = Expression; #[inline] fn sub(self, rhs: Term) -> Expression { Expression::new(vec![-rhs], self as f64) } } impl ops::Sub for Term { type Output = Expression; #[inline] fn sub(self, rhs: Term) -> Expression { Expression::from_terms(vec![self, -rhs]) } } impl ops::Sub for Term { type Output = Expression; #[inline] fn sub(self, mut rhs: Expression) -> Expression { rhs = -rhs; rhs.terms.insert(0, self); rhs } } impl ops::Sub for Expression { type Output = Expression; #[inline] fn sub(mut self, rhs: Term) -> Expression { self -= rhs; self } } impl ops::SubAssign for Expression { #[inline] fn sub_assign(&mut self, rhs: Term) { self.terms.push(-rhs); } } #[cfg(test)] mod tests { use super::*; const LEFT: Variable = Variable::from_id(0); const RIGHT: Variable = Variable::from_id(1); const LEFT_TERM: Term = Term::from_variable(LEFT); const RIGHT_TERM: Term = Term::from_variable(RIGHT); #[test] fn new() { assert_eq!( Term::new(LEFT, 2.0), Term { variable: LEFT, coefficient: 2.0 } ); } #[test] fn from_variable() { assert_eq!( Term::from_variable(LEFT), Term { variable: LEFT, coefficient: 1.0 } ); } #[test] fn mul_f64() { assert_eq!( LEFT_TERM * 2.0, Term { variable: LEFT, coefficient: 2.0 } ); assert_eq!( 2.0 * LEFT_TERM, Term { variable: LEFT, coefficient: 2.0 } ); } #[test] fn mul_f32() { assert_eq!( LEFT_TERM * 2.0f32, Term { variable: LEFT, coefficient: 2.0 } ); assert_eq!( 2.0f32 * LEFT_TERM, Term { variable: LEFT, coefficient: 2.0 } ); } #[test] fn mul_assign_f64() { let mut term = LEFT_TERM; term *= 2.0; assert_eq!( term, Term { variable: LEFT, coefficient: 2.0 } ); } #[test] fn mul_assign_f32() { let mut term = LEFT_TERM; term *= 2.0f32; assert_eq!( term, Term { variable: LEFT, coefficient: 2.0 } ); } #[test] fn div_f64() { assert_eq!( LEFT_TERM / 2.0, Term { variable: LEFT, coefficient: 0.5 } ); } #[test] fn div_f32() { assert_eq!( LEFT_TERM / 2.0f32, Term { variable: LEFT, coefficient: 0.5 } ); } #[test] fn div_assign_f64() { let mut term = LEFT_TERM; term /= 2.0; assert_eq!( term, Term { variable: LEFT, coefficient: 0.5 } ); } #[test] fn div_assign_f32() { let mut term = LEFT_TERM; term /= 2.0f32; assert_eq!( term, Term { variable: LEFT, coefficient: 0.5 } ); } #[test] fn add_f64() { assert_eq!(LEFT_TERM + 2.0, Expression::new(vec![LEFT_TERM], 2.0)); assert_eq!(2.0 + LEFT_TERM, Expression::new(vec![LEFT_TERM], 2.0)); } #[test] fn add_f32() { assert_eq!(LEFT_TERM + 2.0f32, Expression::new(vec![LEFT_TERM], 2.0)); assert_eq!(2.0f32 + LEFT_TERM, Expression::new(vec![LEFT_TERM], 2.0)); } #[test] fn add_term() { assert_eq!( LEFT_TERM + RIGHT_TERM, Expression::from_terms(vec![LEFT_TERM, RIGHT_TERM]) ); } #[test] fn add_expression() { assert_eq!( LEFT_TERM + Expression::new(vec![RIGHT_TERM], 1.0), Expression::new(vec![LEFT_TERM, RIGHT_TERM], 1.0) ); } #[test] fn sub_f64() { assert_eq!(LEFT_TERM - 2.0, Expression::new(vec![LEFT_TERM], -2.0)); assert_eq!(2.0 - LEFT_TERM, Expression::new(vec![-LEFT_TERM], 2.0)); } #[test] fn sub_f32() { assert_eq!(LEFT_TERM - 2.0f32, Expression::new(vec![LEFT_TERM], -2.0)); assert_eq!(2.0f32 - LEFT_TERM, Expression::new(vec![-LEFT_TERM], 2.0)); } #[test] fn sub_term() { assert_eq!( LEFT_TERM - RIGHT_TERM, Expression::from_terms(vec![LEFT_TERM, -RIGHT_TERM]) ); } #[test] fn sub_expression() { assert_eq!( LEFT_TERM - Expression::new(vec![RIGHT_TERM], 1.0), Expression::new(vec![LEFT_TERM, -RIGHT_TERM], -1.0) ); } #[test] fn neg() { assert_eq!( -LEFT_TERM, Term { variable: LEFT, coefficient: -1.0 } ); } } kasuari-0.4.11/src/variable.rs000064400000000000000000000224641046102023000143000ustar 00000000000000use core::ops; #[cfg(not(feature = "portable-atomic"))] use core::sync::atomic::{AtomicUsize, Ordering}; #[cfg(feature = "portable-atomic")] use portable_atomic::{AtomicUsize, Ordering}; use crate::{Expression, Term}; /// Identifies a variable for the constraint solver. /// Each new variable is unique in the view of the solver, but copying or cloning the variable /// produces a copy of the same variable. #[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Debug)] pub struct Variable(usize); impl Variable { /// Produces a new unique variable for use in constraint solving. #[inline] pub fn new() -> Self { static VARIABLE_ID: AtomicUsize = AtomicUsize::new(0); Self(VARIABLE_ID.fetch_add(1, Ordering::Relaxed)) } #[cfg(test)] pub(crate) const fn from_id(id: usize) -> Self { Self(id) } } impl Default for Variable { #[inline] fn default() -> Self { Self::new() } } impl ops::Add for Variable { type Output = Expression; #[inline] fn add(self, constant: f64) -> Expression { Term::from(self) + constant } } impl ops::Add for Variable { type Output = Expression; #[inline] fn add(self, constant: f32) -> Expression { Term::from(self) + constant } } impl ops::Add for f64 { type Output = Expression; #[inline] fn add(self, variable: Variable) -> Expression { Term::from(variable) + self } } impl ops::Add for f32 { type Output = Expression; #[inline] fn add(self, variable: Variable) -> Expression { Term::from(variable) + self } } impl ops::Add for Variable { type Output = Expression; #[inline] fn add(self, other: Variable) -> Expression { Term::from(self) + Term::from(other) } } impl ops::Add for Variable { type Output = Expression; #[inline] fn add(self, term: Term) -> Expression { Term::from(self) + term } } impl ops::Add for Term { type Output = Expression; #[inline] fn add(self, variable: Variable) -> Expression { self + Term::from(variable) } } impl ops::Add for Variable { type Output = Expression; #[inline] fn add(self, expression: Expression) -> Expression { Term::from(self) + expression } } impl ops::Add for Expression { type Output = Expression; #[inline] fn add(self, variable: Variable) -> Expression { self + Term::from(variable) } } impl ops::AddAssign for Expression { #[inline] fn add_assign(&mut self, variable: Variable) { *self += Term::from(variable); } } impl ops::Neg for Variable { type Output = Term; #[inline] fn neg(self) -> Term { -Term::from(self) } } impl ops::Sub for Variable { type Output = Expression; #[inline] fn sub(self, constant: f64) -> Expression { Term::from(self) - constant } } impl ops::Sub for Variable { type Output = Expression; #[inline] fn sub(self, constant: f32) -> Expression { Term::from(self) - constant } } impl ops::Sub for f64 { type Output = Expression; #[inline] fn sub(self, variable: Variable) -> Expression { self - Term::from(variable) } } impl ops::Sub for f32 { type Output = Expression; #[inline] fn sub(self, v: Variable) -> Expression { self - Term::from(v) } } impl ops::Sub for Variable { type Output = Expression; #[inline] fn sub(self, other: Variable) -> Expression { Term::from(self) - Term::from(other) } } impl ops::Sub for Variable { type Output = Expression; #[inline] fn sub(self, term: Term) -> Expression { Term::from(self) - term } } impl ops::Sub for Term { type Output = Expression; #[inline] fn sub(self, variable: Variable) -> Expression { self - Term::from(variable) } } impl ops::Sub for Variable { type Output = Expression; #[inline] fn sub(self, expression: Expression) -> Expression { Term::from(self) - expression } } impl ops::Sub for Expression { type Output = Expression; #[inline] fn sub(self, variable: Variable) -> Expression { self - Term::from(variable) } } impl ops::SubAssign for Expression { #[inline] fn sub_assign(&mut self, variable: Variable) { *self -= Term::from(variable); } } impl ops::Mul for Variable { type Output = Term; #[inline] fn mul(self, coefficient: f64) -> Term { Term::new(self, coefficient) } } impl ops::Mul for Variable { type Output = Term; #[inline] fn mul(self, coefficient: f32) -> Term { Term::new(self, coefficient as f64) } } impl ops::Mul for f64 { type Output = Term; #[inline] fn mul(self, variable: Variable) -> Term { Term::new(variable, self) } } impl ops::Mul for f32 { type Output = Term; #[inline] fn mul(self, variable: Variable) -> Term { Term::new(variable, self as f64) } } impl ops::Div for Variable { type Output = Term; #[inline] fn div(self, coefficient: f64) -> Term { Term::new(self, 1.0 / coefficient) } } impl ops::Div for Variable { type Output = Term; #[inline] fn div(self, coefficient: f32) -> Term { Term::new(self, 1.0 / coefficient as f64) } } #[cfg(test)] mod tests { use alloc::vec; use super::*; const LEFT: Variable = Variable(0); const RIGHT: Variable = Variable(1); const LEFT_TERM: Term = Term::from_variable(LEFT); const RIGHT_TERM: Term = Term::from_variable(RIGHT); #[test] fn variable_default() { assert_ne!(LEFT, RIGHT); } #[test] fn variable_add_f64() { assert_eq!(LEFT + 5.0, Expression::new(vec![LEFT_TERM], 5.0),); assert_eq!(5.0 + LEFT, Expression::new(vec![LEFT_TERM], 5.0),); } #[test] fn variable_add_f32() { assert_eq!(LEFT + 5.0_f32, Expression::new(vec![LEFT_TERM], 5.0),); assert_eq!(5.0_f32 + LEFT, Expression::new(vec![LEFT_TERM], 5.0),); } #[test] fn variable_add_variable() { assert_eq!( LEFT + RIGHT, Expression::new(vec![LEFT_TERM, RIGHT_TERM], 0.0), ); } #[test] fn variable_add_term() { assert_eq!( LEFT + RIGHT_TERM, Expression::new(vec![LEFT_TERM, RIGHT_TERM], 0.0), ); assert_eq!( LEFT_TERM + RIGHT, Expression::new(vec![LEFT_TERM, RIGHT_TERM], 0.0), ); } #[test] fn variable_add_expression() { assert_eq!( LEFT + Expression::from_term(RIGHT_TERM), Expression::new(vec![LEFT_TERM, RIGHT_TERM], 0.0), ); assert_eq!( Expression::from_term(LEFT_TERM) + RIGHT, Expression::new(vec![LEFT_TERM, RIGHT_TERM], 0.0), ); } #[test] fn variable_add_assign() { let mut expression = Expression::from_term(LEFT_TERM); expression += RIGHT; assert_eq!( expression, Expression::new(vec![LEFT_TERM, RIGHT_TERM], 0.0), ); } #[test] fn variable_sub_f64() { assert_eq!(LEFT - 5.0, Expression::new(vec![LEFT_TERM], -5.0),); assert_eq!(5.0 - LEFT, Expression::new(vec![-LEFT_TERM], 5.0),); } #[test] fn variable_sub_f32() { assert_eq!(LEFT - 5.0_f32, Expression::new(vec![LEFT_TERM], -5.0),); assert_eq!(5.0_f32 - LEFT, Expression::new(vec![-LEFT_TERM], 5.0),); } #[test] fn variable_sub_variable() { assert_eq!( LEFT - RIGHT, Expression::new(vec![LEFT_TERM, -RIGHT_TERM], 0.0), ); } #[test] fn variable_sub_term() { assert_eq!( LEFT - RIGHT_TERM, Expression::new(vec![LEFT_TERM, -RIGHT_TERM], 0.0), ); assert_eq!( LEFT_TERM - RIGHT, Expression::new(vec![LEFT_TERM, -RIGHT_TERM], 0.0), ); } #[test] fn variable_sub_expression() { assert_eq!( LEFT - Expression::from_term(RIGHT_TERM), Expression::new(vec![LEFT_TERM, -RIGHT_TERM], 0.0), ); assert_eq!( Expression::from_term(LEFT_TERM) - RIGHT, Expression::new(vec![LEFT_TERM, -RIGHT_TERM], 0.0), ); } #[test] fn variable_sub_assign() { let mut expression = Expression::from_term(LEFT_TERM); expression -= RIGHT; assert_eq!( expression, Expression::new(vec![LEFT_TERM, -RIGHT_TERM], 0.0), ); } #[test] fn variable_mul_f64() { assert_eq!(LEFT * 5.0, Term::new(LEFT, 5.0)); assert_eq!(5.0 * LEFT, Term::new(LEFT, 5.0)); } #[test] fn variable_mul_f32() { assert_eq!(LEFT * 5.0_f32, Term::new(LEFT, 5.0)); assert_eq!(5.0_f32 * LEFT, Term::new(LEFT, 5.0)); } #[test] fn variable_div_f64() { assert_eq!(LEFT / 5.0, Term::new(LEFT, 1.0 / 5.0)); } #[test] fn variable_div_f32() { assert_eq!(LEFT / 5.0_f32, Term::new(LEFT, 1.0 / 5.0)); } #[test] fn variable_neg() { assert_eq!(-LEFT, -LEFT_TERM); } } kasuari-0.4.11/tests/common/mod.rs000064400000000000000000000020201046102023000151170ustar 00000000000000extern crate alloc; use alloc::rc::Rc; use core::cell::RefCell; use hashbrown::HashMap; use kasuari::Variable; #[derive(Clone, Default)] struct Values(Rc>>); impl Values { fn value_of(&self, var: Variable) -> f64 { *self.0.borrow().get(&var).unwrap_or(&0.0) } fn update_values(&self, changes: &[(Variable, f64)]) { for (var, value) in changes { println!("{var:?} changed to {value:?}"); self.0.borrow_mut().insert(*var, *value); } } } #[allow(clippy::type_complexity)] pub fn new_values() -> ( Box f64>, Box, ) { let values = Values(Rc::new(RefCell::new(HashMap::new()))); let value_of = { let values = values.clone(); move |v| values.value_of(v) }; let update_values = { let values = values.clone(); move |changes: &[_]| { values.update_values(changes); } }; (Box::new(value_of), Box::new(update_values)) } kasuari-0.4.11/tests/quadrilateral.rs000064400000000000000000000076001046102023000157130ustar 00000000000000use kasuari::WeightedRelation::*; use kasuari::{Solver, Strength, Variable}; mod common; use common::new_values; #[test] fn test_quadrilateral() { struct Point { x: Variable, y: Variable, } impl Point { fn new() -> Point { Point { x: Variable::new(), y: Variable::new(), } } } let (value_of, update_values) = new_values(); let points = [Point::new(), Point::new(), Point::new(), Point::new()]; let point_starts = [(10.0, 10.0), (10.0, 200.0), (200.0, 200.0), (200.0, 10.0)]; let midpoints = [Point::new(), Point::new(), Point::new(), Point::new()]; let mut solver = Solver::new(); let mut weight = 1.0; let multiplier = 2.0; for i in 0..4 { solver .add_constraints([ points[i].x | EQ(Strength::WEAK * weight) | point_starts[i].0, points[i].y | EQ(Strength::WEAK * weight) | point_starts[i].1, ]) .unwrap(); weight *= multiplier; } for (start, end) in [(0, 1), (1, 2), (2, 3), (3, 0)] { solver .add_constraints([ midpoints[start].x | EQ(Strength::REQUIRED) | ((points[start].x + points[end].x) / 2.0), midpoints[start].y | EQ(Strength::REQUIRED) | ((points[start].y + points[end].y) / 2.0), ]) .unwrap(); } solver .add_constraints([ (points[0].x + 20.0) | LE(Strength::STRONG) | points[2].x, (points[0].x + 20.0) | LE(Strength::STRONG) | points[3].x, (points[1].x + 20.0) | LE(Strength::STRONG) | points[2].x, (points[1].x + 20.0) | LE(Strength::STRONG) | points[3].x, (points[0].y + 20.0) | LE(Strength::STRONG) | points[1].y, (points[0].y + 20.0) | LE(Strength::STRONG) | points[2].y, (points[3].y + 20.0) | LE(Strength::STRONG) | points[1].y, (points[3].y + 20.0) | LE(Strength::STRONG) | points[2].y, ]) .unwrap(); for point in &points { solver .add_constraints([ point.x | GE(Strength::REQUIRED) | 0.0, point.y | GE(Strength::REQUIRED) | 0.0, point.x | LE(Strength::REQUIRED) | 500.0, point.y | LE(Strength::REQUIRED) | 500.0, ]) .unwrap() } update_values(solver.fetch_changes()); assert_eq!( [ (value_of(midpoints[0].x), value_of(midpoints[0].y)), (value_of(midpoints[1].x), value_of(midpoints[1].y)), (value_of(midpoints[2].x), value_of(midpoints[2].y)), (value_of(midpoints[3].x), value_of(midpoints[3].y)) ], [(10.0, 105.0), (105.0, 200.0), (200.0, 105.0), (105.0, 10.0)] ); solver .add_edit_variable(points[2].x, Strength::STRONG) .unwrap(); solver .add_edit_variable(points[2].y, Strength::STRONG) .unwrap(); solver.suggest_value(points[2].x, 300.0).unwrap(); solver.suggest_value(points[2].y, 400.0).unwrap(); update_values(solver.fetch_changes()); assert_eq!( [ (value_of(points[0].x), value_of(points[0].y)), (value_of(points[1].x), value_of(points[1].y)), (value_of(points[2].x), value_of(points[2].y)), (value_of(points[3].x), value_of(points[3].y)) ], [(10.0, 10.0), (10.0, 200.0), (300.0, 400.0), (200.0, 10.0)] ); assert_eq!( [ (value_of(midpoints[0].x), value_of(midpoints[0].y)), (value_of(midpoints[1].x), value_of(midpoints[1].y)), (value_of(midpoints[2].x), value_of(midpoints[2].y)), (value_of(midpoints[3].x), value_of(midpoints[3].y)) ], [(10.0, 105.0), (155.0, 300.0), (250.0, 205.0), (105.0, 10.0)] ); } kasuari-0.4.11/tests/removal.rs000064400000000000000000000013131046102023000145210ustar 00000000000000use kasuari::WeightedRelation::*; use kasuari::{Constraint, Solver, Strength, Variable}; mod common; use common::new_values; #[test] fn remove_constraint() { let (value_of, update_values) = new_values(); let mut solver = Solver::new(); let val = Variable::new(); let constraint: Constraint = val | EQ(Strength::REQUIRED) | 100.0; solver.add_constraint(constraint.clone()).unwrap(); update_values(solver.fetch_changes()); assert_eq!(value_of(val), 100.0); solver.remove_constraint(&constraint).unwrap(); solver .add_constraint(val | EQ(Strength::REQUIRED) | 0.0) .unwrap(); update_values(solver.fetch_changes()); assert_eq!(value_of(val), 0.0); }