linesweeper: keep a copy of the crates.io 0.5.0 package in vendor/

Add the linesweeper 0.5.0 package to vendor/ with no file changed, so
that a fix to it can reach the Craft apps through this repository
without waiting for a linesweeper release. The GitHub repository its
manifest names is archived, and its README says development moved off
GitHub.

Leave out the test font (fonts live in craft-fonts), the logo artwork,
the snapshot images and the package's repository tooling. SOURCE.json
lists each with its hash.

Add a workspace exclude, a [patch.crates-io] entry, SOURCE.json with
the archive hash and per-file hashes, NOTICE and ATTRIBUTION.md rows,
and the package's tests in the backend test script. craft-prepress's
flatten feature now builds against the copy, so Cargo.lock drops the
package's registry source and checksum.
This commit is contained in:
dotCooCoo committed 2026-11-06 19:05:18 -08:00
1 parent fd5e2feff6
commit f14cc7f21e
78 files changed
+21645 -3

No files matched your search

+1
View File
@@ -12,6 +12,7 @@ author, source and license. Library code and original assets are under the repos
| `crates/font-store/index/indexes/google-fonts.json.gz` (unmodified) | craft-font-index contributors; generated from the METADATA.pb files of [google/fonts](https://github.com/google/fonts) at `45e3d5269e4a17d823f6d84470bc084dc5ae46f6` | [aiongg/craft-font-index@5a9a7f8](https://github.com/aiongg/craft-font-index/tree/5a9a7f8038c8f3e2e1583a79c92a816e8e71b34f) | Apache-2.0 |
| `crates/font-store/index/indexes/noto.json.gz` (unmodified) | craft-font-index contributors; generated from [notofonts/notofonts.github.io](https://github.com/notofonts/notofonts.github.io) at `93fac8abebe6c7fed0c5b7d2456d8fa484fa146c` and [noto-cjk](https://github.com/notofonts/noto-cjk) releases | [aiongg/craft-font-index@5a9a7f8](https://github.com/aiongg/craft-font-index/tree/5a9a7f8038c8f3e2e1583a79c92a816e8e71b34f) | Apache-2.0 |
| `crates/cms/tests/data/FOGRA39.txt` (unmodified) | Fogra | [Fogra characterisation data](https://fogra.org/en/downloads/work-tools/characterisation-data), `FOGRA39_to_FOGRA60_v2.zip` | Fogra terms: free use and redistribution, unmodified, with Fogra identified as the source; "FOGRA39" identifies the reference data only and implies no certification, approval or endorsement by Fogra (full text in [NOTICE](NOTICE)) |
| `vendor/linesweeper/tests/regression/`, `vendor/linesweeper/proptest-regressions/`, the `.snap` files under `vendor/linesweeper/src/` and the path data in `vendor/linesweeper/benches/painted_dreams.rs` (all unmodified; hashes in `vendor/linesweeper/SOURCE.json`) | As published in the package, whose manifest names Joe Neeman as author and records no other source | [linesweeper 0.5.0](https://crates.io/crates/linesweeper/0.5.0) | MIT OR Apache-2.0 (the package's `license` field) |
| `crates/i18n/data/collation.postcard` | Unicode, Inc. (ICU 78.1 collation data, CLDR 48.2.1); generated by `crates/i18n/data/generate-collation.sh` | [ICU](https://github.com/unicode-org/icu/releases/tag/release-78.1rc) | Unicode-3.0 |
| `crates/text/assets/hyphen/en-us.dic` | Grady Ward (Moby Hyphenator II), converted by DesignCraft | [Project Gutenberg eBook #3204](https://www.gutenberg.org/ebooks/3204) | Public domain |
| `crates/proof/data/en-words.dic` | Grady Ward (Moby Hyphenator II word list); break points dropped by craft-proof contributors with `crates/proof/examples/en_words.rs` | [Project Gutenberg eBook #3204](https://www.gutenberg.org/ebooks/3204), via craft-i18n `language-data/en-us/en-us.dic` | Public domain |
Generated
-2
View File
@@ -3127,8 +3127,6 @@ checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4"
[[package]]
name = "linesweeper"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a25df1b0c5432893f41bba817150e323703ee5f6d974f3a6808ac7fd7f046d18"
dependencies = [
"arrayvec",
"kurbo 0.13.1",
+2 -1
View File
@@ -1,7 +1,7 @@
[workspace]
resolver = "3"
members = ["crates/*"]
exclude = ["vendor/krilla", "vendor/hayro", "vendor/hayro-interpret", "vendor/hayro-syntax", "vendor/hayro-jbig2", "vendor/hayro-cmap", "vendor/hayro-postscript"]
exclude = ["vendor/krilla", "vendor/hayro", "vendor/hayro-interpret", "vendor/hayro-syntax", "vendor/hayro-jbig2", "vendor/hayro-cmap", "vendor/hayro-postscript", "vendor/linesweeper"]
[workspace.package]
version = "0.1.0"
@@ -83,6 +83,7 @@ hayro-syntax = { path = "vendor/hayro-syntax" }
hayro-jbig2 = { path = "vendor/hayro-jbig2" }
hayro-cmap = { path = "vendor/hayro-cmap" }
hayro-postscript = { path = "vendor/hayro-postscript" }
linesweeper = { path = "vendor/linesweeper" }
ecolor = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
emath = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
epaint = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
+8
View File
@@ -215,6 +215,14 @@ Their licences and per-file hashes are retained in vendor/hayro-interpret/assets
vendor/hayro-cmap/assets, licenses/pdf-backend and ATTRIBUTION.md. These are existing reader dependency assets;
applications retain their own document-font selection and licensing policy.
linesweeper
linesweeper 0.5.0 (https://crates.io/crates/linesweeper/0.5.0), MIT OR Apache-2.0, is stored in
vendor/linesweeper with its upstream license files and no file changed. Its manifest names Joe Neeman
as author, and its LICENSE-MIT reads "Copyright (c) 2018 Raph Levien". See
vendor/linesweeper/README.craft.md and vendor/linesweeper/SOURCE.json for provenance and the files
left out.
Platform lifecycle source provenance
The optional crates/platform services adapt the preserved local platform candidate
+3
View File
@@ -16,3 +16,6 @@ for backend in hayro-syntax hayro-interpret hayro hayro-jbig2 hayro-cmap hayro-p
done
cargo test --manifest-path "$test_root/hayro-syntax/Cargo.toml" --doc "${patches[@]}"
cargo test --manifest-path "$test_root/hayro-syntax/Cargo.toml" --lib --no-default-features --features std "${patches[@]}"
cp -R "$root/vendor/linesweeper" "$test_root/linesweeper"
cargo test --manifest-path "$test_root/linesweeper/Cargo.toml" --locked --lib --test regression
cargo test --manifest-path "$test_root/linesweeper/Cargo.toml" --locked --doc
+6
View File
@@ -0,0 +1,6 @@
{
"git": {
"sha1": "824b0791a750f7b27b8edcbb6c6244d0e629875e"
},
"path_in_vcs": ""
}
+5
View File
@@ -0,0 +1,5 @@
/target
docs/sweep.pdf
result
ignore
.config/selfci/local.yaml
+43
View File
@@ -0,0 +1,43 @@
# Changelog
## 0.5.0 - 2026-10-05
### Changed
- Boolean operations now attempt to preserve quadratic segments: they will no
longer be automatically converted into cubics (issue 2774a82).
### Fixed
- Fixed a panic with polylines that have degenerate segments (issue da2b71b).
- Fixed a panic with intersections exactly at the end of segments (issue f8732c7).
- Fixed a panic (in slow-asserts mode only) from an ordering violation (issue df03063).
- Fixed an infinite loop (issue a140a5f).
### Added
- Added some convenience methods to `Contours` (patch d4a09be).
## 0.4.0 - 2026-06-14
0.4.0 is the first release with a changelog entry; for previous releases, we
didn't have enough users to worry about it.
### Changed
- There is a new "approximate" output mode, which is now the default. Previous
versions of linesweeper provided strong correctness guarantees, but those
guarantees made some cases slow and caused more subdivisions than necessary.
Also, I think that those stronger guarantees are probably not needed for most
applications -- other boolean ops libraries don't provide them as far as I
know. So linesweeper now provides weaker guarantees by default. For the old
behaviour, see `Topology::contours_correct`.
- The winding direction conventions have changed; output curves have been
reversed, so that they agree with `kurbo`'s conventions.
- The `BinaryWindingNumber` API has changed; it now uses the `OneOfTwo` enum to
distinguish the two sets, instead of a `bool`.
### Added
- The new `Topology::has_normal_contours` and `Topology::is_disjoint_tags` allow
for detecting some no-op (or approximate no-op) situations.
+1255
View File
File diff suppressed because it is too large. Load diff
+194
View File
@@ -0,0 +1,194 @@
# 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 = "2024"
name = "linesweeper"
version = "0.5.0"
authors = ["Joe Neeman <joe@neeman.me>"]
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Robust sweep-line algorithm and two-dimensional boolean ops"
readme = "README.md"
license = "MIT OR Apache-2.0"
repository = "https://github.com/jneem/linesweeper"
[features]
arbitrary = ["dep:arbitrary"]
debug-svg = ["dep:svg"]
generators = []
slow-asserts = []
[lib]
name = "linesweeper"
path = "src/lib.rs"
bench = false
[[example]]
name = "boolean_op"
path = "examples/boolean_op.rs"
required-features = ["generators"]
[[example]]
name = "bus"
path = "examples/bus.rs"
[[example]]
name = "curve_metrics"
path = "examples/curve_metrics.rs"
[[example]]
name = "visualize_op"
path = "examples/visualize_op.rs"
[[example]]
name = "winding-numbers"
path = "examples/winding-numbers.rs"
[[test]]
name = "fonts"
path = "tests/fonts.rs"
harness = false
[[test]]
name = "paperjs"
path = "tests/paperjs.rs"
harness = false
[[test]]
name = "regression"
path = "tests/regression.rs"
harness = false
[[test]]
name = "snapshots"
path = "tests/snapshots.rs"
harness = false
[[bench]]
name = "linebender_logo"
path = "benches/linebender_logo.rs"
harness = false
[[bench]]
name = "painted_dreams"
path = "benches/painted_dreams.rs"
harness = false
[[bench]]
name = "synthetic"
path = "benches/synthetic.rs"
harness = false
required-features = ["generators"]
[dependencies.arbitrary]
version = "1.4.1"
features = ["derive"]
optional = true
[dependencies.arrayvec]
version = "0.7.6"
features = ["serde"]
[dependencies.kurbo]
version = "0.13.0"
features = ["serde"]
[dependencies.polycool]
version = "0.4.0"
[dependencies.rustc-hash]
version = "2.1.1"
[dependencies.smallvec]
version = "1.15.1"
[dependencies.svg]
version = "0.18"
optional = true
default-features = false
[dev-dependencies.anyhow]
version = "1.0.80"
[dev-dependencies.arbitrary]
version = "1.4.1"
features = ["derive"]
[dev-dependencies.arbtest]
version = "0.3.2"
[dev-dependencies.assert_matches]
version = "1.5.0"
[dev-dependencies.clap]
version = "4.5.1"
features = ["derive"]
[dev-dependencies.color]
version = "0.3.2"
[dev-dependencies.glam]
version = "0.29.0"
[dev-dependencies.glob]
version = "0.3.2"
[dev-dependencies.gungraun]
version = "0.17.0"
[dev-dependencies.i_overlay]
version = "1.9.4"
[dev-dependencies.insta]
version = "1.41.1"
features = [
"serde",
"ron",
]
[dev-dependencies.libtest-mimic]
version = "0.8.1"
[dev-dependencies.proptest]
version = "1.4.0"
[dev-dependencies.serde]
version = "1.0.216"
features = ["derive"]
[dev-dependencies.serde_json]
version = "1.0"
[dev-dependencies.skrifa]
version = "0.31.0"
[dev-dependencies.svg]
version = "0.18"
default-features = false
[dev-dependencies.tiny-skia]
version = "0.11.4"
[dev-dependencies.usvg]
version = "0.46.0"
default-features = false
[profile.bench]
debug = 2
strip = false
[profile.dev.package."*"]
opt-level = 2
+99
View File
@@ -0,0 +1,99 @@
[package]
name = "linesweeper"
version = "0.5.0"
edition = "2024"
license = "MIT OR Apache-2.0"
authors = ["Joe Neeman <joe@neeman.me>"]
description = "Robust sweep-line algorithm and two-dimensional boolean ops"
repository = "https://github.com/jneem/linesweeper"
[workspace]
members = ["fuzz", "util"]
[workspace.dependencies]
kurbo = "0.13.0"
skrifa = "0.31.0"
svg = { version = "0.18", default-features = false }
usvg = { version = "0.46.0", default-features = false }
[features]
arbitrary = ["dep:arbitrary"]
debug-svg = ["dep:svg"]
slow-asserts = []
generators = []
[dependencies]
arbitrary = { version = "1.4.1", features = ["derive"], optional = true }
arrayvec = { version = "0.7.6", features = ["serde"] }
kurbo = { workspace = true, features = ["serde"] }
polycool = "0.4.0"
rustc-hash = "2.1.1"
smallvec = "1.15.1"
svg = { workspace = true, optional = true }
[dev-dependencies]
anyhow = "1.0.80"
arbitrary = { version = "1.4.1", features = ["derive"] }
arbtest = "0.3.2"
assert_matches = "1.5.0"
clap = { version = "4.5.1", features = ["derive"] }
color = "0.3.2"
glam = "0.29.0"
glob = "0.3.2"
gungraun = "0.17.0"
i_overlay = "1.9.4"
insta = { version = "1.41.1", features = ["serde", "ron"] }
kompari = { git = "https://github.com/linebender/kompari" }
libtest-mimic = "0.8.1"
linesweeper_util = { path = "util" }
path-bool = { git = "https://github.com/GraphiteEditor/Graphite" }
proptest = "1.4.0"
serde = { version = "1.0.216", features = ["derive"] }
serde_json = "1.0"
skrifa.workspace = true
svg.workspace = true
tiny-skia = "0.11.4"
usvg.workspace = true
[profile.dev.package."*"]
opt-level = 2
[profile.bench]
debug = true
strip = false
[lib]
bench = false
[[bench]]
name = "linebender_logo"
harness = false
[[bench]]
name = "synthetic"
harness = false
required-features = ["generators"]
[[bench]]
name = "painted_dreams"
harness = false
[[example]]
name = "boolean_op"
required-features = ["generators"]
[[test]]
name = "regression"
harness = false
[[test]]
name = "snapshots"
harness = false
[[test]]
name = "fonts"
harness = false
[[test]]
name = "paperjs"
harness = false
+202
View File
@@ -0,0 +1,202 @@
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.
+25
View File
@@ -0,0 +1,25 @@
Copyright (c) 2018 Raph Levien
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.
+53
View File
@@ -0,0 +1,53 @@
# Local linesweeper 0.5.0 copy
This is the crates.io linesweeper 0.5.0 package (MIT OR Apache-2.0) with some files left out and no
file changed. `.cargo_vcs_info.json` names the commit it was packaged from,
`824b0791a750f7b27b8edcbb6c6244d0e629875e`. That commit is not in the GitHub repository the manifest
names, which is archived; its README says development moved off GitHub.
Every file here except this one and `SOURCE.json` is byte for byte the file in the `.crate` archive.
`SOURCE.json` records the hash of the `.crate` download (`source_archive_sha256`) and, for every file
kept, the hash in the package (`source_sha256`) and the hash here (`sha256`). `upstream_repository` is
null because no public repository this copy can name holds `upstream_revision`. `local_patches` is
empty.
`omitted_files` lists the 43 files of the package left out here, with their hashes:
- `tests/fonts/Inconsolata-Regular.ttf`: font files live in craft-fonts.
- `docs/assets/logo.svg` and `examples/linebender.svg`: the package gives no license for this artwork
apart from its `license` field.
- `tests/snapshots/`: 12 SVG and 20 PNG files.
- `.config/`, `.helix/`, `.typos.toml`, `.gitmodules`, `flake.nix`, `flake.lock` and `nix/`: the
package's repository tooling.
Only the targets listed under Tests read the font, the logos and the snapshot files, and those targets
do not compile from the published package.
## Using it
The directory is excluded from the workspace. With the root manifest's `[patch.crates-io]` entry,
`craft-prepress` builds its `flatten` feature against this copy. Cargo ignores a dependency's patches,
so a consuming workspace adds its own entry:
```toml
[patch.crates-io]
linesweeper = { git = "https://github.com/storytold/craft-libs", rev = "<rev>" }
```
## Tests
`scripts/test-pdf-backends.sh` runs the package's unit, regression and doc tests. The `craft-prepress`
tests with the `flatten` feature run against this copy. `pdf.yml`'s path filter does not list this
directory, so a pull request that changes only files here starts no workflow. Run the tests from this
directory:
```sh
cargo test --locked --lib --test regression
cargo test --locked --doc
```
`Cargo.toml` leaves out the `kompari`, `linesweeper_util` and `path-bool` dev-dependencies, which
`Cargo.toml.orig` gives as git or path dependencies. The `fonts` and `snapshots` tests, the
`winding-numbers` and `boolean_op` examples and the `linebender_logo` bench do not compile here.
Cargo writes to `target/` in this directory, which the root `.gitignore` excludes.
+30
View File
@@ -0,0 +1,30 @@
# linesweeper: a robust sweep-line algorithm
[![Latest published version.](https://img.shields.io/crates/v/linesweeper.svg)](https://crates.io/crates/linesweeper)
[![Documentation build status.](https://img.shields.io/docsrs/linesweeper.svg)](https://docs.rs/linesweeper)
This rust crate implements a "robust" version of the Bentley-Ottmann sweep-line
algorithm, and uses it to provide various two-dimensional geometric primitives
like boolean operations on sets bounded by Bézier paths.
It is currently in an early beta state.
![Image demonstrating set operations applied to the linebender logo](docs/assets/logo.svg)
## Basic usage
To compute binary operations between curves, use the `binary_op` function, which
takes in two paths and computes some binary operation on them:
```rust
use kurbo::Shape;
use linesweeper::{binary_op, FillRule, BinaryOp};
let square = kurbo::Rect::from_center_size((0.0, 0.0), (2.0, 2.0)).to_path(1e-6);
let circle = kurbo::Circle::new((0.0, 0.0), 1.2).to_path(1e-6);
binary_op(&square, &circle, FillRule::EvenOdd, BinaryOp::Intersection);
```
## Advanced usage
For more advanced usage (like custom n-ary operations) see `Topology`, or
the example in `examples/bus.rs`.
+129
View File
@@ -0,0 +1,129 @@
{
"crate": "linesweeper",
"source_repository": "https://crates.io/api/v1/crates/linesweeper/0.5.0/download",
"source_revision": "0.5.0",
"source_directory": "linesweeper-0.5.0",
"source_archive_sha256": "a25df1b0c5432893f41bba817150e323703ee5f6d974f3a6808ac7fd7f046d18",
"upstream_repository": null,
"upstream_revision": "824b0791a750f7b27b8edcbb6c6244d0e629875e",
"upstream_directory": "",
"local_patches": [],
"files": [
{ "path": ".cargo_vcs_info.json", "source_sha256": "47fba6a9dbdf445a5c97f783c9be62093035f9d5e5cdc2cf3216ad8259bcd8fc", "source_git_blob": null, "sha256": "47fba6a9dbdf445a5c97f783c9be62093035f9d5e5cdc2cf3216ad8259bcd8fc" },
{ "path": ".gitignore", "source_sha256": "88b59092499ae05234330eb7d0b01e953e969caf05261530e3765eef02536cf0", "source_git_blob": null, "sha256": "88b59092499ae05234330eb7d0b01e953e969caf05261530e3765eef02536cf0" },
{ "path": "CHANGELOG.md", "source_sha256": "70df2144c3ab6858e3857e159590116f7859f3b077ac60fd4a2f510707894dc7", "source_git_blob": null, "sha256": "70df2144c3ab6858e3857e159590116f7859f3b077ac60fd4a2f510707894dc7" },
{ "path": "Cargo.lock", "source_sha256": "42d3aea0c577080ff5a23e3213a05a9ca9f0437584ac3f75aeb6d29941417f0c", "source_git_blob": null, "sha256": "42d3aea0c577080ff5a23e3213a05a9ca9f0437584ac3f75aeb6d29941417f0c" },
{ "path": "Cargo.toml", "source_sha256": "2f925d646204059bb3b4e980704b04e9accc5184d9dd24e3ef9ed1751ac6095b", "source_git_blob": null, "sha256": "2f925d646204059bb3b4e980704b04e9accc5184d9dd24e3ef9ed1751ac6095b" },
{ "path": "Cargo.toml.orig", "source_sha256": "9ce4a22c92543eb8a2caa9e59ec1e8933c8975f12bad6fef731a0c0fe9c34ced", "source_git_blob": null, "sha256": "9ce4a22c92543eb8a2caa9e59ec1e8933c8975f12bad6fef731a0c0fe9c34ced" },
{ "path": "LICENSE-APACHE", "source_sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30", "source_git_blob": null, "sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" },
{ "path": "LICENSE-MIT", "source_sha256": "23c23145f6eac25c28cdcc03d38506a5712a9a8cffb5bd5301457f6173ea696e", "source_git_blob": null, "sha256": "23c23145f6eac25c28cdcc03d38506a5712a9a8cffb5bd5301457f6173ea696e" },
{ "path": "README.md", "source_sha256": "cb35a926cacf52c4d423ec192c9c78e5a1f73057a3658f1b9c47f43d2718cb9e", "source_git_blob": null, "sha256": "cb35a926cacf52c4d423ec192c9c78e5a1f73057a3658f1b9c47f43d2718cb9e" },
{ "path": "benches/linebender_logo.rs", "source_sha256": "d10867f43439d37f0e1da5d30eaed9ab3bfbe655ed47212f65dc20ab4db65cb3", "source_git_blob": null, "sha256": "d10867f43439d37f0e1da5d30eaed9ab3bfbe655ed47212f65dc20ab4db65cb3" },
{ "path": "benches/painted_dreams.rs", "source_sha256": "073008db93840d380710e13ed3ae38e08cb823f10c12e5f838dac1ccf37eaecf", "source_git_blob": null, "sha256": "073008db93840d380710e13ed3ae38e08cb823f10c12e5f838dac1ccf37eaecf" },
{ "path": "benches/synthetic.rs", "source_sha256": "7ce59bef2e1c4f28b00cb89feb15bc71ca4b098a8d27e7b02ea699aca9bcc810", "source_git_blob": null, "sha256": "7ce59bef2e1c4f28b00cb89feb15bc71ca4b098a8d27e7b02ea699aca9bcc810" },
{ "path": "docs/sweep.typ", "source_sha256": "4766082c8672a7bb4356b5581043489c13b4e3fb8931ba3a5f0a473fab0172f3", "source_git_blob": null, "sha256": "4766082c8672a7bb4356b5581043489c13b4e3fb8931ba3a5f0a473fab0172f3" },
{ "path": "examples/boolean_op.rs", "source_sha256": "3b38a1ccda7e736cce4b191a3a45ab4824c4c365190e3eb637b2fb39b5996c9f", "source_git_blob": null, "sha256": "3b38a1ccda7e736cce4b191a3a45ab4824c4c365190e3eb637b2fb39b5996c9f" },
{ "path": "examples/bus.rs", "source_sha256": "e206a08be73899f564188052c8ecc15526b54606e52ee2935a64ce6b5723da33", "source_git_blob": null, "sha256": "e206a08be73899f564188052c8ecc15526b54606e52ee2935a64ce6b5723da33" },
{ "path": "examples/curve_metrics.rs", "source_sha256": "0db6a5327fd2119a03a9a66624fea3e04b6bbb519655667ad30487aee6c787d2", "source_git_blob": null, "sha256": "0db6a5327fd2119a03a9a66624fea3e04b6bbb519655667ad30487aee6c787d2" },
{ "path": "examples/visualize_op.rs", "source_sha256": "a5210ed389ceb1e9c43b952b992e29e65065d129d2b2f4be62fffc747376bf79", "source_git_blob": null, "sha256": "a5210ed389ceb1e9c43b952b992e29e65065d129d2b2f4be62fffc747376bf79" },
{ "path": "examples/winding-numbers.rs", "source_sha256": "4a756c908564703b165257001e73d2f7235476cd536580091f3a7a70d2eab242", "source_git_blob": null, "sha256": "4a756c908564703b165257001e73d2f7235476cd536580091f3a7a70d2eab242" },
{ "path": "proptest-regressions/sweep/sweep_line.txt", "source_sha256": "f5311edf1c0c4d1574bb7b24ed1d1c66106d9949214014d0a8479f685938dc98", "source_git_blob": null, "sha256": "f5311edf1c0c4d1574bb7b24ed1d1c66106d9949214014d0a8479f685938dc98" },
{ "path": "proptest-regressions/sweep/weak_ordering.txt", "source_sha256": "b5b183ab470399c15246457d8a1a35cafb49fee120d6777dfdd563bfdbadda52", "source_git_blob": null, "sha256": "b5b183ab470399c15246457d8a1a35cafb49fee120d6777dfdd563bfdbadda52" },
{ "path": "proptest-regressions/topology.txt", "source_sha256": "49ff0b5b370f082984276d5312f03b1fc96ef0dd218f9efd7b084b11abbb9d19", "source_git_blob": null, "sha256": "49ff0b5b370f082984276d5312f03b1fc96ef0dd218f9efd7b084b11abbb9d19" },
{ "path": "src/arbitrary.rs", "source_sha256": "9c3753a40cfea2528a546295d39e0513537db5a3411a931fccea063873d95750", "source_git_blob": null, "sha256": "9c3753a40cfea2528a546295d39e0513537db5a3411a931fccea063873d95750" },
{ "path": "src/curve/line.rs", "source_sha256": "07aebead4b0050410a3f9f28b66062b6016c115df58e2b2669470263cd352f3e", "source_git_blob": null, "sha256": "07aebead4b0050410a3f9f28b66062b6016c115df58e2b2669470263cd352f3e" },
{ "path": "src/curve/mod.rs", "source_sha256": "36a8dbe96ac1d3bec307dc63b504206ec7fdfc11d10799fbb1a8990c29b8c5f2", "source_git_blob": null, "sha256": "36a8dbe96ac1d3bec307dc63b504206ec7fdfc11d10799fbb1a8990c29b8c5f2" },
{ "path": "src/curve/split_quad.rs", "source_sha256": "b8aae4ce7c16fa623724f667e1058130a9c7eed693b5b01e8b9e16632b8ec45f", "source_git_blob": null, "sha256": "b8aae4ce7c16fa623724f667e1058130a9c7eed693b5b01e8b9e16632b8ec45f" },
{ "path": "src/curve/transverse.rs", "source_sha256": "be9c695531f4cb0fe73af13e78040b51b9476c193d84562e701aadd07b3fd06e", "source_git_blob": null, "sha256": "be9c695531f4cb0fe73af13e78040b51b9476c193d84562e701aadd07b3fd06e" },
{ "path": "src/generators.rs", "source_sha256": "cc9f2441546db9204219294c354449809dda77dffad06fec8a30f45df01e6fef", "source_git_blob": null, "sha256": "cc9f2441546db9204219294c354449809dda77dffad06fec8a30f45df01e6fef" },
{ "path": "src/geom.rs", "source_sha256": "9878ffa6d4a13fa2c9149741c137205ca01acb626a5b9f86a3db75cf91b63a08", "source_git_blob": null, "sha256": "9878ffa6d4a13fa2c9149741c137205ca01acb626a5b9f86a3db75cf91b63a08" },
{ "path": "src/lib.rs", "source_sha256": "761c153c3b5c449569ff85c571344b9252ea64f7f0b0424fb4819041b03d13c6", "source_git_blob": null, "sha256": "761c153c3b5c449569ff85c571344b9252ea64f7f0b0424fb4819041b03d13c6" },
{ "path": "src/num.rs", "source_sha256": "39ff85da50c500ce91cf6dea81aedc0dcca1db5e7d0506696dde2da118e93a2a", "source_git_blob": null, "sha256": "39ff85da50c500ce91cf6dea81aedc0dcca1db5e7d0506696dde2da118e93a2a" },
{ "path": "src/order.rs", "source_sha256": "7e5b04eae8af134abb032e995f1d3c2ac473822923dfee8d463d9ade6ffeb596", "source_git_blob": null, "sha256": "7e5b04eae8af134abb032e995f1d3c2ac473822923dfee8d463d9ade6ffeb596" },
{ "path": "src/perturbation.rs", "source_sha256": "fa274a822bbdf65f8e53e403eef78034dd87964b3f1d3134a064df4ec8cef9c5", "source_git_blob": null, "sha256": "fa274a822bbdf65f8e53e403eef78034dd87964b3f1d3134a064df4ec8cef9c5" },
{ "path": "src/position.rs", "source_sha256": "dfa70840805ecccf5b7eb6122921e46ca60c24cb7b3c17f3b55ffc2549368ab0", "source_git_blob": null, "sha256": "dfa70840805ecccf5b7eb6122921e46ca60c24cb7b3c17f3b55ffc2549368ab0" },
{ "path": "src/segments.rs", "source_sha256": "379f9455120492d237cb5a62b3fc06da018a72e2eb75bbfcd61b4cce501d9bbc", "source_git_blob": null, "sha256": "379f9455120492d237cb5a62b3fc06da018a72e2eb75bbfcd61b4cce501d9bbc" },
{ "path": "src/snapshots/linesweeper__tests__empty_horizontals.snap", "source_sha256": "e319881853e889badf6524f19af4cd6f112e8dd71bdc516826f4463134a82244", "source_git_blob": null, "sha256": "e319881853e889badf6524f19af4cd6f112e8dd71bdc516826f4463134a82244" },
{ "path": "src/snapshots/linesweeper__tests__two_squares.snap", "source_sha256": "01961e0311a5af3b8f13eb52a2a7112560c47cf86771c04e40475dc6f15a0846", "source_git_blob": null, "sha256": "01961e0311a5af3b8f13eb52a2a7112560c47cf86771c04e40475dc6f15a0846" },
{ "path": "src/snapshots/linesweeper__topology__tests__diamond.snap", "source_sha256": "cfaefd620a0a3d1376861bca09cd7c658ef00a32635b88f1b259a9b39cd15502", "source_git_blob": null, "sha256": "cfaefd620a0a3d1376861bca09cd7c658ef00a32635b88f1b259a9b39cd15502" },
{ "path": "src/snapshots/linesweeper__topology__tests__inner_loop.snap", "source_sha256": "d6259594f87cc91bf015e9086df94f000c265164d6d361beda4ef0e422947d91", "source_git_blob": null, "sha256": "d6259594f87cc91bf015e9086df94f000c265164d6d361beda4ef0e422947d91" },
{ "path": "src/snapshots/linesweeper__topology__tests__merge_monotonic_segs.snap", "source_sha256": "871db61c9bf44a5d50cd63f3682c72ac19b1ae5d4752d911ac75a2e94c9f8cf5", "source_git_blob": null, "sha256": "871db61c9bf44a5d50cd63f3682c72ac19b1ae5d4752d911ac75a2e94c9f8cf5" },
{ "path": "src/snapshots/linesweeper__topology__tests__merge_monotonic_segs_with_cut.snap", "source_sha256": "0c67eb18012af31d9d5a46ce16e49f3033a4a7bd986bdc41d14cb3d4ce7adf39", "source_git_blob": null, "sha256": "0c67eb18012af31d9d5a46ce16e49f3033a4a7bd986bdc41d14cb3d4ce7adf39" },
{ "path": "src/snapshots/linesweeper__topology__tests__nested_squares.snap", "source_sha256": "212e53e762e633ae8316ce1ba16d94c4f6608573dac6ec3325bf7a9e592c6f8e", "source_git_blob": null, "sha256": "212e53e762e633ae8316ce1ba16d94c4f6608573dac6ec3325bf7a9e592c6f8e" },
{ "path": "src/snapshots/linesweeper__topology__tests__square.snap", "source_sha256": "c0835f86603b9f3aabcb0da22c1dcf597c6575ce9ca72d5ae95bf125f40020e8", "source_git_blob": null, "sha256": "c0835f86603b9f3aabcb0da22c1dcf597c6575ce9ca72d5ae95bf125f40020e8" },
{ "path": "src/snapshots/linesweeper__topology__tests__square_and_diamond.snap", "source_sha256": "6dffc3dc182e133222481d562de20b5e9e39d0ee343088c42f29868d97d1b304", "source_git_blob": null, "sha256": "6dffc3dc182e133222481d562de20b5e9e39d0ee343088c42f29868d97d1b304" },
{ "path": "src/snapshots/linesweeper__topology__tests__square_with_double_back.snap", "source_sha256": "b6709e07eb96d1148269caa9ea39663bb432524e892fffe10f784fa44edb29e0", "source_git_blob": null, "sha256": "b6709e07eb96d1148269caa9ea39663bb432524e892fffe10f784fa44edb29e0" },
{ "path": "src/snapshots/linesweeper__topology__tests__squares_with_gaps.snap", "source_sha256": "52a5e09ecd081f7ace976692a5d5e3549210441f78ad1dc25cd98a4d9d5dceb6", "source_git_blob": null, "sha256": "52a5e09ecd081f7ace976692a5d5e3549210441f78ad1dc25cd98a4d9d5dceb6" },
{ "path": "src/sweep/mod.rs", "source_sha256": "467f783e605be9c5959b10adc3ff53966ca0e95599b3b509bfacb154e82b9257", "source_git_blob": null, "sha256": "467f783e605be9c5959b10adc3ff53966ca0e95599b3b509bfacb154e82b9257" },
{ "path": "src/sweep/output_event.rs", "source_sha256": "220dc6847871ffcd0f782122b55db0a47522866892fb0fb5dd4255cea975fc83", "source_git_blob": null, "sha256": "220dc6847871ffcd0f782122b55db0a47522866892fb0fb5dd4255cea975fc83" },
{ "path": "src/sweep/range.rs", "source_sha256": "924274de47783179f52cea99b370297e8c8d5f05500df4a6ff31e9b927dadd06", "source_git_blob": null, "sha256": "924274de47783179f52cea99b370297e8c8d5f05500df4a6ff31e9b927dadd06" },
{ "path": "src/sweep/snapshots/linesweeper__sweep__sweep_line__tests__conservative_line_order.snap", "source_sha256": "236d020f646edc3ab199888fd52a0e7d9939bbb1d8b13b6fed52ce508c0e6667", "source_git_blob": null, "sha256": "236d020f646edc3ab199888fd52a0e7d9939bbb1d8b13b6fed52ce508c0e6667" },
{ "path": "src/sweep/snapshots/linesweeper__sweep__sweep_line__tests__regression_position_state.snap", "source_sha256": "be9535fe81604b59e369e6e2c61a67e0203429166dd23e51cd90c644fa3e09db", "source_git_blob": null, "sha256": "be9535fe81604b59e369e6e2c61a67e0203429166dd23e51cd90c644fa3e09db" },
{ "path": "src/sweep/snapshots/linesweeper__sweep__sweep_line__tests__square.snap", "source_sha256": "b35c3e6a38d5ecdd8acf315ba0b0bd6485cd8b40193fdd76bd446bdfdb6eda77", "source_git_blob": null, "sha256": "b35c3e6a38d5ecdd8acf315ba0b0bd6485cd8b40193fdd76bd446bdfdb6eda77" },
{ "path": "src/sweep/snapshots/linesweeper__sweep__sweep_line__tests__square_and_diamond.snap", "source_sha256": "3f01a76fb7ff4aef0fd55fd869602d4a2b69fff1bd65632f406b275b5d6af600", "source_git_blob": null, "sha256": "3f01a76fb7ff4aef0fd55fd869602d4a2b69fff1bd65632f406b275b5d6af600" },
{ "path": "src/sweep/snapshots/linesweeper__sweep__sweep_line__tests__two_squares.snap", "source_sha256": "79e3c95e563a66c0ce244125470a106a18dfe29f276b732defd305406c873c7e", "source_git_blob": null, "sha256": "79e3c95e563a66c0ce244125470a106a18dfe29f276b732defd305406c873c7e" },
{ "path": "src/sweep/sweep_line.rs", "source_sha256": "822b261f682911ea0c0370eaf17d9201a1e639b315359dcb6a3b877a229816e3", "source_git_blob": null, "sha256": "822b261f682911ea0c0370eaf17d9201a1e639b315359dcb6a3b877a229816e3" },
{ "path": "src/topology.rs", "source_sha256": "9c9b48bc3853884425127e2d64e8ed957ba0d27e1696e2c457e95282e52d6a5b", "source_git_blob": null, "sha256": "9c9b48bc3853884425127e2d64e8ed957ba0d27e1696e2c457e95282e52d6a5b" },
{ "path": "src/treevec.rs", "source_sha256": "d88b9fbf02fa9f318560e1d483d465555f2b696ae12c00f8e4f5b71ea6779445", "source_git_blob": null, "sha256": "d88b9fbf02fa9f318560e1d483d465555f2b696ae12c00f8e4f5b71ea6779445" },
{ "path": "src/typed_vec.rs", "source_sha256": "3e9a2675b8a40d8fc4df0575bd9d0d16b5604d18e9b78638dd5a612b28fc4c30", "source_git_blob": null, "sha256": "3e9a2675b8a40d8fc4df0575bd9d0d16b5604d18e9b78638dd5a612b28fc4c30" },
{ "path": "tests/fonts.rs", "source_sha256": "081604ebf560a19ff0427efd9a17eaaa0a3b46ad0bf58323fd6e21b6c0764400", "source_git_blob": null, "sha256": "081604ebf560a19ff0427efd9a17eaaa0a3b46ad0bf58323fd6e21b6c0764400" },
{ "path": "tests/paperjs.rs", "source_sha256": "6ff2a3178f13cde3d7304101ae592ae32b4b2734069f5fc56a532a4883d9f782", "source_git_blob": null, "sha256": "6ff2a3178f13cde3d7304101ae592ae32b4b2734069f5fc56a532a4883d9f782" },
{ "path": "tests/regression.rs", "source_sha256": "b2341934aa001ba26c688a1ca3a7ee55d29105d67b6036bb05bb367b69976fce", "source_git_blob": null, "sha256": "b2341934aa001ba26c688a1ca3a7ee55d29105d67b6036bb05bb367b69976fce" },
{ "path": "tests/regression/awesome_0.txt", "source_sha256": "6f7dfbb7ca62463843c058617d557553fdc8d8b3add512bee8f813b37a256611", "source_git_blob": null, "sha256": "6f7dfbb7ca62463843c058617d557553fdc8d8b3add512bee8f813b37a256611" },
{ "path": "tests/regression/graphite_0.txt", "source_sha256": "f5d9e94ae6fab7d29f8a3aa0ccd9d96fc9b709c0ee3fbae8b000bec8c33628b9", "source_git_blob": null, "sha256": "f5d9e94ae6fab7d29f8a3aa0ccd9d96fc9b709c0ee3fbae8b000bec8c33628b9" },
{ "path": "tests/regression/graphite_1.txt", "source_sha256": "96f147b6fc95a71600f60541cc687598e04e9d810dd8a3c57b0f5d8220a72823", "source_git_blob": null, "sha256": "96f147b6fc95a71600f60541cc687598e04e9d810dd8a3c57b0f5d8220a72823" },
{ "path": "tests/regression/graphite_2.txt", "source_sha256": "7c9e32b68efe68bf238ca9f42a5666cc1b792e8f8bfece9a7c128c08734f4072", "source_git_blob": null, "sha256": "7c9e32b68efe68bf238ca9f42a5666cc1b792e8f8bfece9a7c128c08734f4072" },
{ "path": "tests/regression/graphite_3.txt", "source_sha256": "46f66d5d9f753386df2f163dfcda8b40e727350f623e4e2fb309a094912c82ce", "source_git_blob": null, "sha256": "46f66d5d9f753386df2f163dfcda8b40e727350f623e4e2fb309a094912c82ce" },
{ "path": "tests/regression/graphite_4.txt", "source_sha256": "84f07e36e3af11f8946ce00890cf396dca15744c195d51ea173f48f354732245", "source_git_blob": null, "sha256": "84f07e36e3af11f8946ce00890cf396dca15744c195d51ea173f48f354732245" },
{ "path": "tests/regression/graphite_5.txt", "source_sha256": "299d27228f1d22fd1c8d0be895357766a948165def542d54ebc19933361b277a", "source_git_blob": null, "sha256": "299d27228f1d22fd1c8d0be895357766a948165def542d54ebc19933361b277a" },
{ "path": "tests/regression/graphite_6.txt", "source_sha256": "0be4c7579159d43f9a9dc04bbd3341b5ac5fc3e49f3518b278dda23506069301", "source_git_blob": null, "sha256": "0be4c7579159d43f9a9dc04bbd3341b5ac5fc3e49f3518b278dda23506069301" },
{ "path": "tests/regression/graphite_7.txt", "source_sha256": "da02afc16a0336680a65508b4f6a13df5a9af04f05a49c66bd1cfd6975827719", "source_git_blob": null, "sha256": "da02afc16a0336680a65508b4f6a13df5a9af04f05a49c66bd1cfd6975827719" },
{ "path": "tests/regression/sasha_0.txt", "source_sha256": "b4eb70030ce7f8eae68f074e2ce93edb60ec1e6f879a3d8238d58447a9961948", "source_git_blob": null, "sha256": "b4eb70030ce7f8eae68f074e2ce93edb60ec1e6f879a3d8238d58447a9961948" },
{ "path": "tests/snapshots.rs", "source_sha256": "634fdebb7d7c7c70a1b47431a9407be640ea19d6cec79f200555bf2cd2b2dde0", "source_git_blob": null, "sha256": "634fdebb7d7c7c70a1b47431a9407be640ea19d6cec79f200555bf2cd2b2dde0" }
],
"omitted_files": [
{ "path": ".config/selfci/ci.yaml", "source_sha256": "3941bf98a6cffce943f7a041ab520626f853090f1e8bd0e2c3df2672c5fa064f" },
{ "path": ".gitmodules", "source_sha256": "704763bd0f97af4af5f5fad27e9a4a6d6d1fe77c375cf6d5f917dd089e069fb3" },
{ "path": ".helix/languages.toml", "source_sha256": "194817d23a58f1b399040b223e9da175433bb21ab20785a011c44524475598c6" },
{ "path": ".typos.toml", "source_sha256": "8cba77a39a9e49ba79635a7da8aa0592072806590aa28c27194b9ba5d53ed6fb" },
{ "path": "docs/assets/logo.svg", "source_sha256": "79857f5989a6310e6990e41e171dbb8ec2acfc6d6b28383d2ef4c844bbc758a4" },
{ "path": "examples/linebender.svg", "source_sha256": "3fe69ac9aeeb62011157e96ea4185e016574b24489606010b97dc7eab0920679" },
{ "path": "flake.lock", "source_sha256": "244d67f7aae447190e6c15fdfc9132c190f97568f2c67dee1d9a2e06982c9b1f" },
{ "path": "flake.nix", "source_sha256": "43c8c31bb919717276309ae7e3d7e329dbe641002669177d6f42fcc7ebdf24d5" },
{ "path": "nix/gungraun-runner.nix", "source_sha256": "72701531fbd34ee859b2e467f2c7e47ee5f4ed3a2c938c07ba66fe5911c37537" },
{ "path": "nix/kompari.nix", "source_sha256": "e22dbf5db27c79f423855571f11cc47dd90defe17d8e32423571cf7da3956174" },
{ "path": "tests/fonts/Inconsolata-Regular.ttf", "source_sha256": "0f11ac40d1618e1aa1adb7322a14980a0b0c943421124022073f83236d15f0d5" },
{ "path": "tests/snapshots/inputs/output/merge_back.svg", "source_sha256": "2a41a52604734f49b89b04e5895e02d31554bfce4dce84519553316663b07169" },
{ "path": "tests/snapshots/inputs/position/close_ranges.svg", "source_sha256": "57634495e5922d24e3dde387f3cdf0d741a5724ecc55ff53da2713e11049df38" },
{ "path": "tests/snapshots/inputs/position/do_not_cancel.svg", "source_sha256": "21d05393ced07150213bfa4a8ab459e95fdd23724b4691cd75e33aa53e03f44c" },
{ "path": "tests/snapshots/inputs/position/more_overlapping_close_ranges.svg", "source_sha256": "dd71994d18fe9a664a863f8d69e770fe9c0a939bb74e6990b8f12ce946fb0c67" },
{ "path": "tests/snapshots/inputs/position/near_horizontal_approximation.svg", "source_sha256": "38f1a168754fee189248bf18eef0826b6b12c4af26a346a08df75390d23c56be" },
{ "path": "tests/snapshots/inputs/position/overlapping_close_ranges.svg", "source_sha256": "ec6fd75e4f42a48f460705d601daa4294062753dbb7fb62b682ea05fd071f623" },
{ "path": "tests/snapshots/inputs/position/transverse.svg", "source_sha256": "1e55f07b3fd6aa738bb5c412f833bdd041d6a6259a5f08635d6278301746687b" },
{ "path": "tests/snapshots/inputs/position/transverse_triangle.svg", "source_sha256": "22e09ffc98035e81b081c03ff978734b09baeca3f24efb29319c7f2f229f3c76" },
{ "path": "tests/snapshots/inputs/sweep/just_cross.svg", "source_sha256": "f0fbd780f2272be21080f3570b6c915c5da172ae39efbc86afd30d9d8713e20e" },
{ "path": "tests/snapshots/inputs/sweep/just_touch.svg", "source_sha256": "3d6116a9849c9e5b6881a868d24fa617c772f9ffdeddb4328fa2b5024306c42c" },
{ "path": "tests/snapshots/inputs/sweep/square_diamond_cross.svg", "source_sha256": "aacfe86de2509680eddfba260c8c3782e1e6980ffde321e628106257975e6ee3" },
{ "path": "tests/snapshots/inputs/sweep/square_diamond_touch.svg", "source_sha256": "eefe49aa759bc8dcc811c46b6c5b36ebbed2e6efaba14c0c513265d76abd7f1f" },
{ "path": "tests/snapshots/snapshots/correct/output/merge_back.png", "source_sha256": "edf5d1fe510003e2d2099351c35f6af709d54457b3087f3dd6803df5d78482bf" },
{ "path": "tests/snapshots/snapshots/correct/position/close_ranges.png", "source_sha256": "902613b28f47e8619df72f389190d5b9673ba1197a817988b9a0afb52e901a83" },
{ "path": "tests/snapshots/snapshots/correct/position/do_not_cancel.png", "source_sha256": "ce8ceb7bc8ebb812440bcdc8f1100b4062b74d469b741dfa2fcf67261b9c7ee3" },
{ "path": "tests/snapshots/snapshots/correct/position/more_overlapping_close_ranges.png", "source_sha256": "3d7b5d648a3d6591687f4161419cd26ef88d92677d5abee357ce1e6d81d748ad" },
{ "path": "tests/snapshots/snapshots/correct/position/near_horizontal_approximation.png", "source_sha256": "21f275cd6a16ab81c4f297c8c2bcf1e1ed60d49ac8cee44299ef7e5944fd14c6" },
{ "path": "tests/snapshots/snapshots/correct/position/overlapping_close_ranges.png", "source_sha256": "4bf65eea3df7e4d609adf1b60f7c37335bdbdfaaeca08c10e50002bacdf49b16" },
{ "path": "tests/snapshots/snapshots/correct/position/transverse.png", "source_sha256": "8a49c69b088b56ae0df17f5e2f2bd2e063d11cbf50e21e34fbd75fd6c58e22aa" },
{ "path": "tests/snapshots/snapshots/correct/position/transverse_triangle.png", "source_sha256": "4a5a1afc404308f7a1cbeb7e8ed72ace41c892f6847669c5ff7f78c422088428" },
{ "path": "tests/snapshots/snapshots/output/merge_back.png", "source_sha256": "071563db38f07c93bb9f70cb45a3976d140bc188751d07a17c344ebfb1f57b49" },
{ "path": "tests/snapshots/snapshots/position/close_ranges.png", "source_sha256": "8f41f087d9779932a3be26be29e2912eb174b78cb859ecf99f2e9756cc810620" },
{ "path": "tests/snapshots/snapshots/position/do_not_cancel.png", "source_sha256": "2366066007ee5b26d075f5f3107432d0f6d0eb81089b75604df2880a31d725de" },
{ "path": "tests/snapshots/snapshots/position/more_overlapping_close_ranges.png", "source_sha256": "d77b06c175765dd4d29f28106c56a7aaa7feb582a0d88468d04d26ea24184e17" },
{ "path": "tests/snapshots/snapshots/position/near_horizontal_approximation.png", "source_sha256": "855311f5cdaff8cc02f6e44c4bf77b0b1d69d77d1a31f9623976680b9a9111e4" },
{ "path": "tests/snapshots/snapshots/position/overlapping_close_ranges.png", "source_sha256": "14dd185567f7732a4e518a0f3714ff9cd1831fc2e042eafc110b85e0ca3b4f1b" },
{ "path": "tests/snapshots/snapshots/position/transverse.png", "source_sha256": "0f564234c7dc5a25cdb05e1848445d2ca46b20b18732390f0960de421784a1bc" },
{ "path": "tests/snapshots/snapshots/position/transverse_triangle.png", "source_sha256": "a1e98a59561505d8497548f28ed31913b23908d7bde4bf06b533009e317fc924" },
{ "path": "tests/snapshots/snapshots/sweep/just_cross.png", "source_sha256": "f66ee5cbad2129bead4a35a63924f7c016b9a6c21f3c659118821294f27f8879" },
{ "path": "tests/snapshots/snapshots/sweep/just_touch.png", "source_sha256": "a33d963ceb32e7d3f1cffc013564303b96a1e02c68f51041687d7831ed0359b3" },
{ "path": "tests/snapshots/snapshots/sweep/square_diamond_cross.png", "source_sha256": "63e097c08279cbb0efad3813641e5a70924af16a69ac9893ca1727f256c6f762" },
{ "path": "tests/snapshots/snapshots/sweep/square_diamond_touch.png", "source_sha256": "8d0b7f9adbba5f7c35745466357b814dc11e46b0c0662bbdfec229bd4bc1e0b5" }
]
}
+70
View File
@@ -0,0 +1,70 @@
use gungraun::{library_benchmark, library_benchmark_group, main};
use std::hint::black_box;
use kurbo::BezPath;
use linesweeper::{BinaryOp, FillRule, Segments, binary_op, topology::Topology};
use linesweeper_util::svg_to_bezpaths;
fn linebender_path() -> BezPath {
let input = include_str!("../examples/linebender.svg");
let tree = usvg::Tree::from_str(input, &usvg::Options::default()).unwrap();
svg_to_bezpaths(&tree)
.into_iter()
.flatten()
.collect::<BezPath>()
}
fn linebender_paths() -> (BezPath, BezPath) {
let input = include_str!("../examples/linebender.svg");
let tree = usvg::Tree::from_str(input, &usvg::Options::default()).unwrap();
let bezpaths = svg_to_bezpaths(&tree);
let first_path = bezpaths.first().unwrap().clone();
let second_path = bezpaths[1..]
.iter()
.flat_map(|b| b.elements().iter().cloned())
.collect();
(first_path, second_path)
}
fn linebender_segments() -> Segments {
let mut segs = Segments::default();
segs.add_bez_path(&linebender_path()).unwrap();
segs
}
#[library_benchmark]
#[bench::bench(setup = linebender_segments)]
fn just_the_sweep(segs: Segments) {
// If we don't collect the events, it seems like nothing gets measured (even
// sprinkling black_box around).
let mut events = Vec::new();
linesweeper::sweep::sweep(&segs, 0.01, |_, ev| {
events.push(ev);
});
black_box(events);
}
#[library_benchmark]
#[bench::bench(setup = linebender_path)]
fn build_topology(path: BezPath) {
black_box(Topology::from_paths_binary(&path, &BezPath::new(), 0.01)).unwrap();
}
#[library_benchmark]
#[bench::bench(setup = linebender_paths)]
fn xor_linesweeper((first_path, second_path): (BezPath, BezPath)) {
black_box(binary_op(
&first_path,
&second_path,
FillRule::EvenOdd,
BinaryOp::Xor,
))
.unwrap();
}
library_benchmark_group!(
name = bench_linebender_logo;
benchmarks = just_the_sweep, build_topology, xor_linesweeper,
);
main!(library_benchmark_groups = bench_linebender_logo);
+26
View File
@@ -0,0 +1,26 @@
use gungraun::{library_benchmark, library_benchmark_group, main};
use kurbo::BezPath;
use linesweeper::{BinaryOp, FillRule, binary_op_with_eps};
fn painted_dreams_paths(eps: f64) -> (BezPath, BezPath, f64) {
let path_a = BezPath::from_svg("M0,340C161.737914,383.575765 107.564182,490.730587 273,476 C419,463 481.741198,514.692273 481.333333,768 C481.333333,768 -0,768 -0,768 C-0,768 0,340 0,340 Z").unwrap();
let path_b = BezPath::from_svg(
"M458.370270,572.165771C428.525848,486.720093 368.618805,467.485992 273,476 C107.564178,490.730591 161.737915,383.575775 0,340 C0,340 0,689 0,689 C56,700 106.513901,779.342590 188,694.666687 C306.607422,571.416260 372.033966,552.205139 458.370270,572.165771 Z",
).unwrap();
(path_a, path_b, eps)
}
#[library_benchmark]
#[bench::inaccurate(setup = painted_dreams_paths, args = (1e-4))]
#[bench::accurate(setup = painted_dreams_paths, args = (1e-6))]
fn painted_dreams((a, b, eps): (BezPath, BezPath, f64)) {
binary_op_with_eps(&a, &b, FillRule::EvenOdd, BinaryOp::Union, eps).unwrap();
}
library_benchmark_group!(
name = bench_painted_dreams;
benchmarks = painted_dreams,
);
main!(library_benchmark_groups = bench_painted_dreams);
+65
View File
@@ -0,0 +1,65 @@
use std::hint::black_box;
use gungraun::{library_benchmark, library_benchmark_group, main};
use kurbo::BezPath;
use linesweeper::{
BinaryOp, FillRule, Point, binary_op,
generators::{checkerboard, slanted_checkerboard, slanties},
};
fn to_bez(points: &[Vec<Point>]) -> BezPath {
fn one(points: &[Point]) -> BezPath {
let mut points = points.iter();
let p = points.next().unwrap();
let mut ret = BezPath::default();
ret.move_to(p.to_kurbo());
for q in points {
ret.line_to(q.to_kurbo());
}
ret.line_to(p.to_kurbo());
ret
}
let mut ret = BezPath::default();
for ps in points {
ret.extend(one(ps));
}
ret
}
fn generate_checkerboard(size: usize) -> (BezPath, BezPath) {
let (contours_even, contours_odd) = checkerboard(size);
(to_bez(&contours_even), to_bez(&contours_odd))
}
fn generate_slanted_checkerboard(size: usize) -> (BezPath, BezPath) {
let (contours_even, contours_odd) = slanted_checkerboard(size);
(to_bez(&contours_even), to_bez(&contours_odd))
}
fn generate_slanties(size: usize) -> (BezPath, BezPath) {
let (contours_even, contours_odd) = slanties(size);
(to_bez(&contours_even), to_bez(&contours_odd))
}
#[library_benchmark]
#[bench::small_checkerboard(setup = generate_checkerboard, args = (10))]
#[bench::medium_checkerboard(setup = generate_checkerboard, args = (20))]
#[bench::large_checkerboard(setup = generate_checkerboard, args = (50))]
#[bench::small_slanted_checkerboard(setup = generate_slanted_checkerboard, args = (10))]
#[bench::medium_slanted_checkerboard(setup = generate_slanted_checkerboard, args = (20))]
#[bench::large_slanted_checkerboard(setup = generate_slanted_checkerboard, args = (50))]
#[bench::small_slanties(setup = generate_slanties, args = (10))]
#[bench::medium_slanties(setup = generate_slanties, args = (20))]
#[bench::large_slanties(setup = generate_slanties, args = (50))]
fn xor((a, b): (BezPath, BezPath)) {
black_box(binary_op(&a, &b, FillRule::EvenOdd, BinaryOp::Xor)).unwrap();
}
library_benchmark_group!(
name = bench_synthetic;
benchmarks = xor,
);
main!(library_benchmark_groups = bench_synthetic);
+960
View File
@@ -0,0 +1,960 @@
#import "@preview/ctheorems:1.1.3": *
#import "@preview/cetz:0.4.0"
#import "@preview/lovelace:0.3.0": *
#set par(justify: true)
#set math.equation(numbering: "(1)")
#show: thmrules
#let lemma = thmbox("lemma", "Lemma")
#let def = thmbox("lemma", "Definition")
#let proof = thmproof("proof", "Proof")
#let remark = thmbox("lemma", "Remark")
#let inexact(term) = {
block(inset: 16pt,
block(
inset: 16pt,
stroke: 1pt + gray,
radius: 12pt,
text(
size: 10pt,
[Note for inexact arithmetic: #term]
)
)
)
}
// TODO: figure out how to get rid of the ":" in the caption
#let invariant(term) = {
figure(
block(inset: 16pt,
text(
size: 10pt,
[#term]
)
),
kind: "invariant",
supplement: "invariant",
caption: ""
)
}
#let fl = text([fl])
We'll be talking about sweep line algorithms, where the sweep line is horizontal and increasing in $y$.
Therefore, every line segment "starts" at the coordinate with smaller $y$ and "ends" at the coordinate
with larger $y$ (we'll assume for now that there are no horizontal segments). We'll parametrize each
line segment as a function of $y$. So if $alpha: [y_0 (alpha), y_1 (alpha)] arrow bb(R)$ is a line segment
then $alpha(y)$ is the $x$ coordinate at $y$-coordinate $y in [y_0 (alpha), y_1 (alpha)]$.
We write $theta_alpha$ for the angle that $alpha$ makes with the positive horizontal axis.
Let's have a picture. (In the discussion, it won't matter whether positive $y$ points up or down, but in the
pictures we'll adopt the graphics convention of having positive $y$ point down.)
#figure(
cetz.canvas({
import cetz.draw: *
line((1, 3), (0, 0), name: "a")
line((-4, 3), (4, 3), stroke: (dash: "dotted"))
line((-4, 0), (4, 0), stroke: (dash: "dotted"))
content((-4, 3), $y_0(alpha)$, anchor: "east")
content((-4, 0), $y_1(alpha)$, anchor: "east")
content((0.6, 1.5), $alpha$, anchor: "west")
cetz.angle.angle("a.start", "a.end", (4, 3), label: $theta_alpha$, label-radius: 0.8)
}),
caption: "A segment and its angle"
)
We'll be dealing with inexact arithmetic, so let's define some "error bars" on our line segments.
For an error parameter $epsilon > 0$, offsetting from $alpha$ by $plus.minus epsilon$ in the perpendicular-to-$alpha$ direction
is the same as offsetting by $alpha plus.minus epsilon / (|sin theta_alpha|)$ in the horizontal direction.
Roughly speaking, the "error bars" on $alpha$ amount to adding this horizontal error. But we'll be slightly
more accurate around the corners, by truncating these error bars to the horizontal extents of $alpha$. Precisely, we define
$
alpha_(+,epsilon)(y) = min(alpha(y) + epsilon / (|sin theta_alpha|), max(alpha(y_0), alpha(y_1)) + epsilon) \
alpha_(-,epsilon)(y) = max(alpha(y) - epsilon / (|sin theta_alpha|), min(alpha(y_0), alpha(y_1)) - epsilon) \
$
In pictures, the gray shaded region is the region between $alpha_(-,epsilon)$ and $alpha_(+,epsilon)$:
The point of the scaling by $|sin theta_alpha|$ is to make this an approximation of an $epsilon$-neighborhood (in the
perpendicular direction) of the line segment. The truncation near the corners ensures that if $x$ is between
$alpha_(-,epsilon)(y)$ and $alpha_(+,epsilon)(y)$ then it is within $sqrt(2) epsilon$ of $alpha$.
#figure(
cetz.canvas({
import cetz.draw: *
line((0.5, 3), (-0.3, 0.6), (-0.3, 0), (0.5, 0), (1.3, 2.4), (1.3, 3), close: true, fill: gray, stroke: 0pt)
line((0.5, 3), (-0.3, 0.6), (-0.3, 0), stroke: ( dash: "dashed" ))
line((0.5, 0), (1.3, 2.4), (1.3, 3), stroke: ( dash: "dashed" ))
line((1, 3), (0, 0), name: "a")
line((-4, 3), (4, 3), stroke: (dash: "dotted"))
line((-4, 0), (4, 0), stroke: (dash: "dotted"))
content((-4, 3), $y_0(alpha)$, anchor: "east")
content((-4, 0), $y_1(alpha)$, anchor: "east")
content((0.6, 1.5), $alpha$, anchor: "west")
content((0.8, 0.5), $alpha_(+,epsilon)$, anchor: "west")
content((0.2, 2.4), $alpha_(-,epsilon)$, anchor: "east")
}),
caption: "A segment and its error bars"
)<fig-error-bars>
Define a relation $prec_(y,epsilon)$ on line segments whose domain contains $y$, where
$alpha prec_(y,epsilon) beta$ if $alpha_(+,epsilon)(y) < beta_(-,epsilon)(y)$.
Intuitively, $alpha prec_(y,epsilon) beta$ if $alpha$ is definitely to the left of $beta$
at height $y$: $alpha$ is far enough to the left that their error bars don't overlap.
Clearly, for a given $y$ and $epsilon$ there are three mutually exclusive possibilities: either
$alpha prec_(y,epsilon) beta$ or $beta prec_(y,epsilon) alpha$ or neither of the two holds. We'll denote
this third possibility by $alpha approx_(y,epsilon) beta$, and we write
$alpha prec.tilde_(y,epsilon) beta$ for "$alpha prec_(y,epsilon) beta$ or $alpha approx_(y,epsilon) beta$."
Here are a few basic properties of our definitions:
#lemma[
1. For any $y$ and any $epsilon > 0$, $prec_(y,epsilon)$ is transitive:
if $alpha prec_(y,epsilon) beta$
and $beta prec_(y,epsilon) gamma$ then $alpha prec_(y,epsilon) gamma$. (However, $prec.tilde_(y,epsilon)$ is not transitive.)
2. For any $y$ and any $epsilon > 0$,
if $alpha prec_(y,epsilon) beta$
and $beta prec.tilde_(y,epsilon) gamma$ then $alpha prec.tilde_(y,epsilon) gamma$.
3. For any $y$ and any $epsilon > 0$,
if $alpha prec.tilde_(y,epsilon) beta$
and $beta prec_(y,epsilon) gamma$ then $alpha prec.tilde_(y,epsilon) gamma$.
4. For any $y$, the relation $prec_(y,epsilon)$ is monotone in $epsilon$, in that if $alpha prec_(y,epsilon) beta$ then $alpha prec_(y,eta) beta$ for
any $eta in (0, epsilon)$.
]<lem-basic-order-properties>
Since $epsilon$ for us will usually be fixed, we will often drop it from the notation, and write $alpha_-$ and $alpha_+$ instead
of $alpha_(-,epsilon)$ and $alpha_(+,epsilon)$.
#def[
Suppose $alpha$ and $beta$ are two segments whose domain contains $y$. We say that *$(alpha, beta)$
$epsilon$-cross by $y$* if $y$ belongs to both domains and $alpha succ_(y,epsilon) beta$.
We say that *$(alpha, beta)$ $epsilon$-cross* if they $epsilon$-cross by $min(y_1 (alpha), y_1 (beta))$.
When $epsilon$ is zero, we leave it out: we say that $(alpha, beta)$ cross by $y$ if they $0$-cross by $y$.
]
Note that the definition of $epsilon$-crossing is not symmetric: $(alpha, beta)$ $epsilon$-crossing is
not the same as $(beta, alpha)$ $epsilon$-crossing. We will usually talk about $(alpha, beta)$ $epsilon$-crossing
in the context that $alpha$ starts off to the left of $beta$, and in this context "$(alpha, beta)$ $epsilon$-cross" means
that at some height before the end of $alpha$ and $beta$, $alpha$ has definitely crossed to the right of $beta$.
== Partially ordered sweep-lines
At our first pass, we won't try to detect intersections at all. Instead, we'll produce
a continuum of sweep-lines (constant except at a finite number of points) that *approximately*
track the horizontal order of the segments.
#def[
The ordered collection $(alpha^1, ..., alpha^m)$ of line segments is #emph[$epsilon$-ordered at $y$]
if each $alpha^i$ has $y$ in its domain and $alpha^i prec.tilde_(y,epsilon) alpha^j$ for all $1 <= i < j <= m$.
]
Our algorithm will produce a family of sweep-lines that are $epsilon$-ordered at every $y$
(and also the sweep-lines will be #emph[complete] in the sense that the sweep-line at $y$ will contain all line segments whose
domain contains $y$). This seems weaker than finding all the intersections (for example, because if you
find all intersections you can use them to produce a completely ordered family of sweep-lines), but
in fact they're more-or-less equivalent: given weakly-ordered sweep-lines, you can perturb the lines so
that your weak order becomes the real order of the perturbed lines.
#lemma[
If $(alpha^1, ..., alpha^m)$ is $epsilon$-ordered at $y$ then there exist $x^1 <= ... <= x^m$ such that
$alpha_(-,epsilon)^i (y) <= x^i <= alpha_(+,epsilon)^i (y)$ for all $i$.
]<lem-horizontal-realization>
#proof[
Define $x^i = max_(j <= i) alpha_(-,epsilon)^j (y)$.
Since we've included $j = i$, this clearly satisfies $x^i >= alpha_(-,epsilon)^j (y)$.
For the other inequality, the ordering condition implies that $alpha^j_(-,epsilon) (y) <= alpha^i_(+,epsilon) (y)$
for every $j <= i$. Therefore this inequality still holds for the maximum over these $j$.
]
One consequence of our approximate approach is that we need to do a little extra bookkeeping to maintain
the continuity of the input paths: when one segment exits and its path-neighbor enters, we need to remember
that they are connected because the approximate sweep-line might not keep them together. We'll ignore this
bookkeeping for now;
the goal here is to get into detail about the sweep-line invariants, and prove that we can maintain them.
== The sweep-line invariants
We're going to have a sweep-line that depends on $y$. When we need to emphasize this, we'll use the
cumbersome but explicit notation
$(alpha_y^1, ..., alpha_y^(m_y))$.
In addition to the sweep-line, we'll maintain a queue of "events." The events are ordered by $y$-position
and they are similar to the classical Bentley-Ottmann events so we'll skimp on the details here. There
is an "enter" event, an "exit" event, and an "intersection" event.
Our sweep-line will maintain two invariants:
+ At every $y$, the sweep-line is $epsilon$-ordered at $y$. (We'll call this the "order" invariant.)
+ For every $y$ and every $1 <= i < j <= m_y$, if $alpha_y^i$ and $alpha_y^j$ $epsilon$-cross
then the event queue contains an event between $i$ and $j$,
and at least one of these events occurs before the $epsilon$-crossing height, or at $y$.
(We'll call this the "crossing" invariant.)
(When we say the event queue contains an event between $i$ and $j$, we mean that either there's
an exit event for some $alpha^k$ with $i < k < j$ or there's an intersection event for some $alpha^k$ and $alpha^ell$
with $i <= k < ell <= j$.)
Hopefully the first invariant is already well-motivated, so let's discuss the second.
To naively ensure that we find
all intersections, we could adopt a stronger crossing invariant that requires all pairs of intersecting segments
in the sweep-line to immediately put an intersection event in the queue. This would be inefficient to maintain because
it would require testing all pairs of segments. The main Bentley-Ottmann observation is that it's enough to have intersection
events for all sweep-line-adjacent pairs, because any pair of lines will have to become sweep-line adjacent strictly
before they intersect. We can't rely only on sweep-line adjacency because of the $epsilon$ fudge, but our "crossing"
event essentially encodes the same property. Note that if $j = i+1$ and $y$ is just before the $epsilon$-crossing height and
there are no other segments nearby, then the mysterious $j'$ must be either $i$ or $j$ (because there is nothing in between)
and the mysterious queue event must be the crossing of $i$ and $j$ (it couldn't be an exit event, because we assumed they
cross and they must cross before they exit). In other cases, the crossing event ensures that even if we haven't
recorded the upcoming crossing of $alpha^i$ and $alpha^j$, something will happen in between them that will give us
a chance to test their crossing.
== Sweeping the sweep line
The first observation is that the sweep line invariants are maintained as we increase $y$ continuously up
to the next event:
+ For the order invariant, first note that there is an event whenever $y$ leaves the domain of a segment, so $y$ remains in all domains until the next event.
Moreover, if at any $y' > y$ the ordering breaks, two line segments must have $epsilon$-crossed one another by $y'$.
The third invariant guarantees that there's an event before this happens, so by the contra-positive until an event happens the ordering
constraint is maintained.
+ The crossing invariant is maintained because the set of things to check (i.e. the set of line segments that $epsilon$-cross
one another after $y$) only shrinks as $y$ increases.
== Interaction with adjacent segments
In vanilla Bentley-Ottmann, each segment gets compared to its two sweep-line neighbors; they can either intersect or not intersect.
When numerical errors are taken into account, we may need to compare to
more segments.
#figure(
cetz.canvas({
import cetz.draw: *
let calc_eps(x0, x1) = 0.5 * calc.sqrt(1 + calc.pow(calc.abs(x1 - x0) / 2, 2))
let mkline(x0, x1) = {
let eps = calc_eps(x0, x1)
line((x0, 1), (x1, -1))
line((x0 + eps, 1), (x1 + eps, -1), stroke: ( thickness: 0.2pt, dash: "dashed" ))
line((x0 - eps, 1), (x1 - eps, -1), stroke: ( thickness: 0.2pt, dash: "dashed" ))
}
let a0 = -8
let a1 = 8
let b0 = -2
let b1 = 2
let c0 = 0.5
let c1 = 0.5
content((a0, 1), $alpha^1$, anchor: "south", padding: 5pt)
content((b0, 1), $alpha^2$, anchor: "south", padding: 5pt)
content((c0, 1), $alpha^3$, anchor: "south", padding: 5pt)
mkline(a0, a1)
mkline(b0, b1)
mkline(c0, c1)
let a_eps = calc_eps(a0, a1)
let b_eps = calc_eps(b0, b1)
let c_eps = calc_eps(c0, c1)
let cross12 = 1 - 2*(b0 + b_eps - a0 + a_eps) / (b0 - a0 - (b1 - a1))
let cross13 = 1 - 2*(c0 + c_eps - a0 + a_eps) / (c0 - a0 - (c1 - a1))
line((-10, cross13), (10, cross13), stroke: ( thickness: 0.2pt ))
line((-10, cross12), (10, cross12), stroke: ( thickness: 0.2pt ))
}),
caption: [Three segments all mixed up. The upper horizontal line is the height at which $(alpha^1, alpha^3)$ $epsilon$-cross,
and the lower horizontal line is the height at which $(alpha^1, alpha^2)$ $epsilon$-cross.]
)<mixed-up-segments>
For example, in @mixed-up-segments imagine that $alpha^1$ and $alpha^2$
are present in the sweep line, and we just added $alpha^3$. If we compare
$alpha^3$ to $alpha^2$, we'll see that the $epsilon$-cross and so we'll add an
intersection event between them sometime betfore they $epsilon$-cross. But if
that's all we do then we're in trouble, because by the time we come around to
looking at that intersection event $alpha^1$ may have already $epsilon$-crossed
$alpha^3$, breaking the ordering invariant. Note that $alpha^2$ doesn't
$epsilon$-cross $alpha^3$ at all in @mixed-up-segments, so there's no guarantee of
an intersection event between those two.
Fix $y$ and $epsilon$, and
suppose we have a collection of lines $(alpha^i, ..., alpha^n)$ that satisfy the ordering invariant
at $y$, and suppose also that $(alpha^(i+1), ..., alpha^n)$
satisfy the crossing invariant at $y$.
To make the whole collection satisfy both invariants, we run the following algorithm.
We call this an *intersection scan to the right*.
#figure(
pseudocode-list([
+ *for* $j = i+1$ up to $n$
+ #line-label(<w-def>) let $w^j$ be the smallest height of any event between $i$ and $j$
+ #line-label(<crossing-test>) *if* $(alpha^i, alpha^j_(+,epsilon))$ cross by $w^j$
+ choose $z$ before the crossing, such that $alpha^i approx_(z,epsilon) alpha^j$
+ insert an intersection event for $(alpha^i, alpha^j)$ at $z$
+ #line-label(<protect>) *if* $alpha^i (z) <= alpha^j_-(z)$ for all $z in [y, w^j]$
+ *break*
]),
caption: "Intersection scan to the right"
)
#inexact[
The test at @protect can be seen as an early-stopping optimization, and is not necessary for correctness.
In particular, an approximation with no false positives
is also fine.
]
#lemma[
Suppose that $(alpha^i, ..., alpha^n)$ satisfy the ordering invariant
at $y$, and suppose also that $(alpha^(i+1), ..., alpha^n)$
satisfy the crossing invariant at $y$.
After running an intersection scan to the right,
$(alpha^i, ..., alpha^n)$ satisfy the crossing invariant at $y$.
In fact, $(alpha^i, ..., alpha^n)$ satisfy a slightly stronger crossing invariant at $y$: for every $j > i$,
if $(alpha^i, alpha^j_(+,epsilon))$ cross then the event queue contains an event between $i$ and $j$, and before
the crossing height.
]<lem-intersection-scan>
(The special thing about the stronger crossing invariant is that it asks whether
$(alpha^i, alpha^j_(+,epsilon))$ cross, where the usual crossing invariant asks
whether
$(alpha^i_(-,epsilon), alpha^j_(+,epsilon))$ cross.)
#proof[
It suffices to check the stronger crossing invariant.
So take some $k > i$
that $(alpha^i, alpha^k_(+,epsilon))$ cross. We consider two cases: whether or not the loop terminated
before reaching $k$.
- Suppose the loop terminated at some $j < k$. If $(alpha^i, alpha^k_(+,epsilon))$ cross
after $w^j$, then the definition of $w^j$ ensures that there is an event between $i$ and $j$ (and therefore between
$i$ and $k$) before the crossing. On the other hand, the termination condition ensures that
$alpha^i (z) <= alpha^j_-(z)$ until $w^j$, and so if $(alpha^i, alpha^k_(+,epsilon))$ cross before $w^j$ then
also $(alpha^j, alpha^k)$ cross before that. In this case, the crossing invariant for $(alpha^(i+1), ..., alpha^n)$
implies the existence of an event between $j$ and $k$ (and therefore between $i$ and $k$) before the crossing.
- If the loop included the case $j = k$, we break into two more cases:
- If $(alpha^i, alpha^k_(+,epsilon))$ cross by $w^j$, then the algorithm inserted a witnessing event between $i$ and $j$.
- Otherwise, the definition of $w^j$ ensures that there is an event between $i$ and $j$ before the crossing.
]
#remark[
We can tweak the algorithm a little to try and reduce the number of comparisons.
For a start, if we add an intersection event between $i$ and $j$ then we can
set $w^j$ to $z$, the height of that new event (because $z$ is the new smallest
height of any event between $i$ and $j$).
Therefore, if we broaden the test @crossing-test to check whether $(alpha^i,
alpha^j_-)$ cross, and we choose $z$ to be before the crossing of $(alpha^i,
alpha^j_-)$ then we can skip the test at @protect and terminate straight away.
Thus, there is no loop at all: we only need to consider $j = i + 1$.
One potential issue with this is finding a height $z$ with $alpha^i approx_(z,epsilon) alpha^j$
that's before $(alpha^i, alpha^j_-)$ cross; in other words, we want a height after $(alpha^i_+, alpha^j_-)$
cross but before $(alpha^i, alpha^j_-)$ cross. If $alpha^j$ is almost horizontal, this window might be very
small (or maybe even not representable with our restricted precision).
It may still be worthwhile to have a fast path with a single comparison, and a slow path with a loop.
]
As you might have already guessed, we can also intersection scan to the left; it's pretty much a reflection
of the other one.
#figure(
pseudocode-list([
+ *for* $j = i$ down to $1$
+ let $w^j$ be the smallest height of any event between $j$ and $i$
+ *if* $(alpha^j_(-,epsilon), alpha^i)$ cross by $w^j$
+ choose $z$ before the crossing, such that $alpha^j approx_(z,epsilon) alpha^i$
+ insert an intersection event for $(alpha^j, alpha^i)$ at $z$
+ *if* $alpha^j_+ (z) <= alpha^i (z)$ for all $z in [y, w^j]$
+ *break*
]),
caption: "Intersection scan to the left"
)
We'll skip the proof of the following lemma, because it's basically the same as the last one.
#lemma[
Suppose that $(alpha^1, ..., alpha^i)$ satisfy the ordering invariant
at $y$, and suppose also that $(alpha^1, ..., alpha^(i+1))$
satisfy the crossing invariant at $y$.
After running an intersection scan to the left,
$(alpha^1, ..., alpha^(i+1))$ satisfy a slightly stronger crossing invariant at $y$: for every $j <= i$,
if $(alpha^j_(-,epsilon), alpha^(i+1))$ cross then the event queue contains an event between $j$ and $i+1$, and before
the crossing height.
]<lem-intersection-scan-left>
The purpose of the stronger crossing invariant comes in the next lemma, which deals with scanning in both directions
and allows the insertion of a segment in the middle of a sweep-line.
#lemma[
Suppose that $(alpha^1, ..., alpha^n)$ satisfy the ordering invariant at $y$, and suppose that
$(alpha^1, ..., alpha^i)$ and $(alpha^(i+1), ..., alpha^n)$ satisfy the crossing invariant at $y$.
Let $beta$ be another segment and assume that $(alpha^1, ..., alpha^i, beta, alpha^(i+1), ... alpha^n)$
satisfy the ordering invariant at $y$. After running an intersection scan to the left and an intersection
scan to the right from $beta$,
$(alpha^1, ..., alpha^i, beta, alpha^(i+1), ... alpha^n)$ satisfies the crossing invariant at $y$.
]<lem-intersection-scan-bidirectional>
#proof[
@lem-intersection-scan implies that $(beta, alpha^(i+1), dots, alpha^n)$ satisfies the crossing invariant,
and
@lem-intersection-scan-left implies that $(alpha^1, ..., alpha^i, beta)$ does also. It only remains
to consider interactions between a segment before $beta$ and one after. So fix $j <= i < k$ and suppose
that $(alpha^j, alpha^k)$ $epsilon$-cross. If they $epsilon$-cross after $y_1(beta)$ then $beta$ exit
event witnesses the crossing invariant, so assume that $(alpha^j, alpha^k)$ $epsilon$-cross by $y_1(beta)$.
Then $(alpha^j_-, alpha^k_+)$ cross by $y_1(beta)$, and so one of them crosses $beta$ before $(alpha^j, alpha^k)$ $epsilon$-cross.
If $(alpha^j_-, beta)$ cross then @lem-intersection-scan-left implies that there is an event between $alpha^j$ and $beta$ (and
therefore between $alpha^j$) and $alpha^k$ before the crossing height; otherwise, $(beta, alpha^k_+)$ cross
and so @lem-intersection-scan provides the required event.
]
One last observation in this section, that follows trivially from the algorithm:
#lemma[
If an intersection scan inserts an intersection event for $(alpha, beta)$
then the intersection event's height $z$ satisfies $alpha approx_(z,epsilon) beta$.
]<lem-valid-intersection-events>
== An "enter" event
When inserting a new segment into the current sweep-line, we first choose its sweep-line position using
a binary search on its horizontal coordinate. Let's write $(alpha^1, dots, alpha^m)$ for the sweep-line
before inserting the new segment, and let's call the new segment $beta$. First, we observe that
there is a place to insert the new segment while preserving the ordering invariant.
#lemma[
Suppose $(alpha^1, ..., alpha^m)$ is $epsilon$-ordered at $y$, and let $i$ be the largest $j$ for which
$alpha^j prec_(y,epsilon) beta$. Then
$(alpha^1, ..., alpha^i, beta, alpha^(i+1), ..., alpha^m)$ is $epsilon$-ordered at $y$.
(Here, we can allow the corner cases $i = 0$ and $i = m$ by declaring that
"$alpha^0$" is a vertical line at $x = -infinity$ and "$alpha^(m+1)$" is a vertical line at $x = infinity$).
]<lem-insert-preserving-order>
#proof[
Since $(alpha^1, ..., alpha^m)$ was $epsilon$-ordered at $y$, it suffices to compare beta with all $alpha^j$.
For $i + 1 <= j <= m$, our choice of $i$ immediately implies that $alpha^j succ.tilde_(y,epsilon) beta$.
So consider $1 <= j <= i$. Since $(alpha^1, ..., alpha^m)$ is $epsilon$-ordered, $alpha^j prec.tilde_(y,epsilon) alpha^i$.
Since $alpha^i prec_(y,epsilon) beta$, @lem-basic-order-properties implies that $alpha^j prec.tilde_(y,epsilon) beta$.
]
#inexact[
@lem-insert-preserving-order guarantees the existence of an insertion point, but it doesn't say how to
find it efficiently.
But consider a predicate $f(alpha^j)$ that returns true whenever $alpha^j prec_(y,epsilon) beta$, and
false whenever $alpha^j_+(y) > beta_+(y)$. Running a binary search with this predicate will find some $i$
for which $f(alpha^i)$ is true and $f(alpha^(i+1))$ is false. By scanning to the right from there, we can
find the largest such $i$.
This $i$ is at least as large as the $i$ in @lem-insert-preserving-order, so
to check that it's a valid insertion point we only need to check that it isn't too large. So if $1 <= j <= i$ then
$alpha^j prec.tilde_(y,epsilon) alpha^i$ and so $alpha^j_-(y) <= alpha^i_+(y)$. On the other hand, $f(alpha^i)$
was true and so $alpha^i_+ <= beta_+(y)$. Putting these together shows that $alpha^j prec.tilde_(y,epsilon) beta$.
This algorithm can be implemented with approximate arithmetic, and its running time is logarithmic in the
total length of the sweep line, plus linear in the number of elements that are very close to $beta$.
]
@lem-insert-preserving-order implies that we can insert a new segment while preserving the ordering invariant. By
@lem-intersection-scan-bidirectional, running an intersection scan restores the crossing invariant.
Thus, we can insert a new segment while preserving the sweep-line invariants.
== An "exit" event
When a segments exits the sweep-line, the ordering invariant clearly doesn't break.
Regarding the crossing invariant, it can only break because of $epsilon$-crossing pairs whose
crossing invariant was witnessed by the exit event that was just processed.
To restore the crossing invariant, we need to enqueue some new intersection events.
Let $(alpha^1, ..., alpha^m)$ be the sweep-line after removing the just-exited segment
which, we assume, used to live between $alpha^i$ and $alpha^(i+1)$. Note that both
$(alpha^1, ..., alpha^i)$ and $(alpha^(i+1), ..., alpha^n)$ satisfy the crossing invariant.
By @lem-intersection-scan-bidirectional, running an intersection scan from $alpha^i$ in both
directions restores the crossing invariant. (Technically, this isn't covered by the statement
of @lem-intersection-scan-bidirectional -- which involves a new segment $beta$ -- but the proof
is basically the same.)
== An "intersection" event
Suppose our sweep-line is $(alpha^1, ..., alpha^m)$ and we've just encountered an intersection event for $(alpha^i, alpha^j)$
at height $y$.
If $i > j$ then they've already been swapped in our sweep-line, so we don't need to swap them again. If $i < j$, we need to
swap them. According to @lem-valid-intersection-events, $alpha^j prec.tilde_(y,epsilon) alpha^i$. It seems reasonable, therefore,
to reorder the sweep line by putting $alpha^i$ after $alpha^j$, like
$
alpha^1, ..., alpha^(i-1), alpha^(i+1), ..., alpha^j, alpha^i, alpha^(j+1), ... alpha^n.
$
The issue with this is that $prec.tilde_(y,epsilon)$ is that it's changed the order of pairs other than $alpha^i$ and $alpha^j$:
for every $i < k < j$, the ordering between $alpha^i$ and $alpha^k$ has been swapped. If $prec.tilde$ were transitive, this would
be fine. But it isn't.
To fix this issue, we allow $alpha^i$ to "push" some of the intermediate segments along with it. It's a bit tedious to write (and read)
this precisely, so hopefully this description is enough: for each $k$ between $i$ and $j$, if $alpha^i prec_(y,epsilon) alpha^k$ then
we also move $alpha^k$ after $alpha^j$. Also, we preserve the relative order of all segments moved in this way.
To see that this "pushing" maintains the ordering invariants, note that by definition it preserves the ordering for all comparisons
with $alpha^i$: if some $alpha^ell$ was pushed along with $alpha^i$ then their relative orders haven't changed; and if $alpha^ell$
wasn't pushed then $alpha^ell prec.tilde_(y,epsilon) alpha^i$ and the new order is fine.
What about other pairs? If $alpha^k$ and $alpha^ell$
changed orders then one of them ($alpha^ell$, say) was pushed and the other wasn't. Then $alpha^i prec_(y,epsilon) alpha^ell$
by our choice of the pushed segments, and $alpha^k prec.tilde_(y,epsilon) alpha^i$ by the previous paragraph. Putting these
together, @lem-basic-order-properties implies that $alpha^k prec.tilde_(y,epsilon) alpha^ell$ and so the new order is ok.
#inexact[
We might not be able to tell exactly which segments $alpha^k$ between $alpha^i$ and $alpha^j$ satisfy
$alpha^i prec_(y,epsilon) alpha^k$. Fortunately, we can push a few too many segments while still maintaining
correctness: it suffices to
include all segments $alpha^k succ_(y,epsilon) alpha^i$, while also ensuring that we only include $alpha^k$ for which
$alpha^k_+(y) >= alpha^i_+(y)$.
]
Finally, we need to maintain the crossing invariant. We can probably be more efficient about this, but one
way to be correct is to treat the various swappings as a bunch of deletions from the sweep-line followed by
a bunch of insertions into the sweep-line. We have already shown that running an intersection scan
after each insertion and deletion correctly maintains the crossing invariant, so we can just do that.
= Correctness of the output
We've described how to turn a bunch of segments into a continuum of sweep-lines, but we probably actually wanted
to find the segments' intersection points. Let's define exactly what that means and how to get there.
We'll assume that we start with one or more polylines that may or may not be closed. Equivalently, each segment
comes with an orientation and (optionally) a "following" segment whose starting point is the ending point of the
current segment.
Our goal is to subdivide all segments at all intersection points, meaning that for each segment we'll produce
a polyline; we'll call the segments in each such polyline "output segments," to distinguish them from
the original segments. We require three properties:
- "approximation": for each input segment, every vertex in its output polyline must be within $epsilon$ of the original segment.
Also the first and last vertices in the output polyline must be within $epsilon$ of the start and end of the segment, respectively.
- "continuity": if a segment has a following segment, then the last vertex in its output polyline must coincide
with the first vertex of the following segment's output polyline.
- "completeness": output segments intersect only at their endpoints, unless they are identical.
In the solution we describe, we'll achieve both continuity and the second half
of approximation by insisting that output polylines have exactly the same start-
and endpoints as their input segment. This stronger condition makes the algorithm
simpler, but it also subjectively can hurt output "quality" by requiring more segments.
== The "dense" version
Suppose we've already found a weakly-ordered sweep line at every height. We say
a height is "important" if the sweep line changed at that height, either because
some segments entered or exited, or because the order changed. The naive version
of our subdivision algorithm will subdivide every segment at every important
height. Note that because the sweep line changes at a discrete set of heights,
at every important height $y$ we actually have two weakly-ordered sweep line:
the "old" one from infinitesmally before $y$ height and the "new" one from $y$
onwards. Because everything is continuous and the weak ordering conditions are
closed, both the old and new sweep lines satisfy the weak ordering condi0.2tions
at $y$.
The algorithm goes like this: at every important height we use
@lem-horizontal-realization to assign horizontal positions to segments in the
old sweep line; we subdivide each segment at the resulting coordinate. Then we
use @lem-horizontal-realization again to assign horizontal positions to segments
in the new sweep line. If a segment gets assigned two different horizontal positions
by the old and new sweep lines, we add a horizontal segment between them. Also,
if a segment starts or ends at the current height and its assigned position is not the
same as its starting or ending position, we fix that up with another horizontal segment.
Finally, we subdivide all horizontal segments as necessary to ensure that they only
intersect at endpoints.
I think it's pretty clear that this algorithm is correct. The approximation property
holds (with $sqrt 2 epsilon$ instead of $epsilon$, but that doesn't matter) because
@lem-horizontal-realization always puts a new point within its segment's upper and lower bounds,
and the bounds are chosen to be within $sqrt 2 epsilon$ of the segment; see @fig-error-bars.
As mentioned above, the "continuity" property and the other half of "approximation" follow
because we insisted on including the segment endpoints exactly.
For the "completeness" property, let's focus on the bit between the important heights
(because everything gets subdivided at the important heights, and so maintaining the
completeness property there is just a matter of splitting into enough horizontal segments).
Between two important heights, the sweep line is constant, so the new sweep line
at the old height is the same as the old sweep line at the new height. Thus, all output
subsegments have the same order when leaving the old sweep line as they do when entering
the new sweep line. Therefore any two of them that intersect between the two important
heights must be identical.
== The "sparse" version
We'd like to avoid subdividing every segment at every important height. This basically
involves detecting which segments need to be divided and ignoring the rest. It's implemented
but not yet written up (TODO).
== A more detailed insertion algorithm
It took me a few tries to get the approximate insertion algorithm working, so
I figured it was worth writing up a few more details. First of all, a slight
tweak of @lem-insert-preserving-order shows that there is a position $i$ such
that $alpha^j_- (y) <= beta(y)$ for all $j <= i$ and $alpha^j_+ (y) >= beta(y)$
for all $j > i$. We will look for this $i$, but with some slight slack in our
arithmetic. The slack will be small enough to guarantee we find a position that
satisfies the weaker properties of @lem-insert-preserving-order.
First, we consider a predicate $p(j)$ that returns `false` whenever $alpha^j_+
(y) > beta(y)$. We put no conditions on the values of $j$ for which $p(j)$
returns `true`; a tighter predicate will give a more efficient algorithm, but
doesn't affect correctness. Let $i_-$ be such that $p(i_-) = #true$ and $p(i_-
+ 1) = #false$ (where we allow $i_- = -1$, and handle the boundary cases by
declaring that $p(-1) = #true$ and $p(m+1) = #false)$. Note that $i_-$ can be
found with a binary search.
The definition of $p$ ensures that $alpha^(i_-)_+(y) <= beta(y)$. Under the
assumption that $(alpha^1, ..., alpha^m)$ is weakly ordered, it follows that
for every $j <= i_-$, $alpha^j_-(y) <= beta(y)$. That is, the $i$ that we are
looking for is larger than or equal to $i_-$.
The next step is a linear scan up from $i_-$. Let $q(j)$ be a predicate
that returns `true` whenever $alpha^j_- (y) < beta^j (y)$ and `false` whenever
$alpha^j_- (y) > beta^j_+ (y)$. Take $i >= i_-$ to be the index just before $q$
first returns false; we will insert $beta$ after $alpha^i$.
Let's check that this insertion position is valid: for every $j <= i$ we have
either $j <= i_-$ or $i_- < j <= i$. In the first case, we already observed
that $alpha^j_-(y) <= beta(y)$; in the second case, $q(j)$ returned `true` and
so $alpha^j_-(y) <= beta^j^+ (y)$. In either case, $alpha^j prec.tilde beta$.
On the other hand, $q(i+i)$ being `false` implies that $alpha^(i+1)_- (y) >=
beta(y)$. This implies that for all $j >= i+1$, $alpha^j_+ (y) >= beta(y)$ and
so $alpha^j succ.tilde_y beta$.
= Accuracy analysis
We're going to implement some algorithms in floating point. Suppose $p$ is the number of mantissa bits
of the floating-point type, and let $fl(t)$ denote the rounding function. Let $epsilon_0 = 2^(-1-p)$;
this is the relative error of the rounding function for all normal numbers: for a real number $x$,
as long as $fl(x)$ is finite and non-subnormal,
$
fl(x) = x plus.minus epsilon_0 |x|.
$
Now, if the result of floating point addition or subtraction is subnormal then it is exact; therefore,
for any floating-point $x$ and $y$, if $x - y$ and $x + y$ don't overflow then after rounding they
have relative error at most $epsilon_0$.
#lemma[
If $y_0 <= y <= y_1$ are floating-point numbers and $epsilon_0 <= 1/2$ then computing $(y - y_0) / (y_1 - y_0)$
the naive way gives an absolute error of
at most $3 epsilon_0 + 4 epsilon_0^2$.
]<lem-ratio-error>
#proof[
The assumption $y_0 <= y <= y_1$ ensures that $y - y_0$ and $y_1 - y_0$ do not overflow. Therefore
$
fl(y - y_0) &= (y - y_0) (1 plus.minus epsilon_0) \
fl(y + y_0) &= (y + y_0) (1 plus.minus epsilon_0)
$
and so
$
fl(y - y_0) /
fl(y_1 - y_0) = (y - y_0) / (y_1 - y_0) times
(1 plus.minus epsilon_0) / (1 plus.minus epsilon_0).
$
For the upper bound,
$
(1 + epsilon_0) / (1 - epsilon_0) = (1 + epsilon_0) (1 + epsilon_0 + epsilon_0^2 / (1 - epsilon_0))
= 1 + 2 epsilon_0 + 2 / (1 - epsilon_0) epsilon_0^2 <= 1 + 2 epsilon_0 + 4 epsilon_0^2.
$
For the lower bound,
$
(1 - epsilon_0) / (1 + epsilon_0) = 1 - epsilon_0 - (epsilon_0 (1-epsilon_0)) / (1+epsilon_0) >= 1 - 2 epsilon_0.
$
Putting these together,
$
fl(y - y_0) /
fl(y_1 - y_0) = (y - y_0) / (y_1 - y_0) (1 plus.minus (2 epsilon_0 + 4 epsilon_0^2)).
$
Since $0 <= (y - y_0) / (y_1 - y_0) <= 1$, this relative error is also an absolute error:
$
fl(y - y_0) /
fl(y_1 - y_0) = (y - y_0) / (y_1 - y_0) plus.minus (2 epsilon_0 + 4 epsilon_0^2).
$
The final rounding step adds another absolute error of at most $epsilon_0$.
]
#lemma[
Suppose that $x_0$, $x_1$, and $M$ are floating-point numbers with $|x_0|, |x_1| <= M/2$.
Let $0 <= t <= 1$ be a real number, and let $0 <= hat(t) <= 1$ be a floating point number
with $|t - hat(t)| <= epsilon$. Then computing $x_0 + t (x_1 - x_0)$ the obvious way gives an
absolute error of at most $(3 epsilon_0 + epsilon) M$.
]<lem-convex-combination-error>
#proof[
Let's first consider the error introduced by approximating $t (x_1 - x_0)$ with $hat(t) fl(x_1 - x_0)$:
$
hat(t) fl(x_1 - x_0) = hat(t) (x_1 - x_0)(1 plus.minus epsilon_0) = hat(t) (x_1 - x_0) plus.minus epsilon_0 M,
$
where the second approximation follows because $0 <= hat(t) <= 1$ and $|x_1 - x_0| <= M$.
Since $hat(t) = t plus.minus epsilon$, we continue with
$
hat(t) fl(x_1 - x_0)
= (t plus.minus epsilon) (x_1 - x_0) plus.minus epsilon_0 M,
= t (x_1 - x_0) plus.minus (epsilon_0 + epsilon) M.
$
Rounding the left hand side introduces a relative error of at most $epsilon_0$, which is an absolute error of at most $M epsilon_0$. Therefore,
$
fl(hat(t) fl(x_1 - x_0))
= t (x_1 - x_0) plus.minus (2 epsilon_0 + epsilon) M.
$
Finally, we subtract both sides from $x_0$ and round one more time. This last rounding introduces a relative error of at most $epsilon_0$,
which again leads to an absolute error of at most $2 epsilon_0 M$ (and I think maybe the 2 is unnecessary, but let's not worry).
]
Next, let's analyze the computation of the $y$ intersection point.
#lemma[
Suppose we have two segments $alpha$ and $beta$. Set $y_0 = y_0(alpha) or y_0(beta)$ and $y_1 = y_1(alpha) and y_1(beta)$
(these can be computed exactly). Suppose that we can compute $alpha(y)$ and $beta(y)$ to additive accuracy $epsilon$,
and assume that $beta(y_0) - alpha(y_0) >= 0$ and
$
alpha(y_1) - beta(y_1) > 16/3 (epsilon + 2 epsilon_0 M).
$
Define $t = (alpha(y_1) - beta(y_1)) / (alpha(y_1) - beta(y_1) + beta(y_0) - alpha(y_0)$ and set $y = y_0 + t(y_1 - y_0)$. The
natural way to compute $y$ produces an approximation $hat(y)$ satisfying
$
|alpha(hat(y)) - beta(hat(y))| <= 4(epsilon + 2 epsilon_0 M) (1 + max(1/m_alpha, 1/m_beta)),
$
where $m_alpha$ and $m_beta$ are the slopes of $alpha$ and $beta$.
]<lem-intersection-height-error>
Basically, this says we can find a good approximate crossing height as long as the two segments cross by at least $16/3 (epsilon + 2 epsilon_0 M)$.
#proof[
First, note that at least one of $alpha(y_0)$ or $beta(y_0)$ is calculated exactly; similarly for $alpha(y_1)$ and $beta(y_1)$.
Therefore the absolute error in calculating $alpha(y_1) - beta(y_1)$ is at most $epsilon + epsilon_0 M$ (where the $epsilon_0 M$ comes
from rounding after the summation) and the absolute error in calculating
$(alpha(y_1) - beta(y_1)) + (beta(y_0) - alpha(y_0))$ is at most $2 epsilon + 3 epsilon_0 M$.
These errors aren't independent, though, since both terms involve a computation of $alpha(y_1) - beta(y_1)$.
Specifically, let $c = alpha(y_1) - beta(y_1)$ and let $d = (alpha(y_1) - beta(y_1)) + (beta(y_0) - alpha(y_0))$; suppose
our calculation of $c$ has error $epsilon_c$ and our calculation of $d$ has error $epsilon_d$. Then
$
|epsilon_c - epsilon_d| <= epsilon + 2 epsilon_0 M,
$ <eq-error-cancellation>
because this is the *additional* error introduced into $d$ by computing $beta(y_0) - alpha(y_0)$ and adding it to the other term.
Now we consider the quotient
$
(c + epsilon_c) / (d + epsilon_d)
&= c / (d (1 + epsilon_d/d)) + epsilon_c / (d (1 + epsilon_d/d)) \
&= c / d (1 - epsilon_d/d + (epsilon_d/d)^2 / (1 + epsilon_d/d)) + epsilon_c / d (1 - (epsilon_d/d)/(1 + epsilon_d / d)).
$
We've assumed that $1 + epsilon_d/d >= 1/2$, and so the error
$
abs((c + epsilon_c) / (d + epsilon_d) - c/d)
$
is at most
$
& abs(c / d ( - epsilon_d/d + (epsilon_d/d)^2 / (1 + epsilon_d/d)) + epsilon_c / d (1 - (epsilon_d/d)/(1 + epsilon_d / d))) \
& <= abs(- c / d epsilon_d/d + epsilon_c / d ) + 2 c/d (epsilon_d/d)^2 + 2 abs(epsilon_c epsilon_d) / d^2 \
& <= c/d abs(epsilon_c/d - epsilon_d/d) + (1 - c/d) abs(epsilon_c/d) + 2 (epsilon_d/d)^2 + 2 abs(epsilon_c epsilon_d) / d^2,
$
where in the last line we recall that $0 <= c/d <= 1$. Now, recall from @eq-error-cancellation and the paragraph before it
that both $abs(epsilon_c)$ and $abs(epsilon_c - epsilon_d)$ are bounded by $epsilon + 2 epsilon_0 M$, and that
$abs(epsilon_d)$ is bounded by $2 epsilon + 3 epsilon_0 M$. Therefore, our computation of $c/d$ (which, recall, we called $t$ above) has absolute error of at most
$
(epsilon + 2 epsilon_0 M)/d + 4 (2 epsilon + 3 epsilon_0 M)^2 / d^2.
$
Let's call this expression $epsilon_t$.
By @lem-convex-combination-error, the computation of $y = y_0 + t (y_1 - y_0)$ has absolute error at most $(2 epsilon_0 + epsilon_t) |y_1 - y_0| + epsilon_0 M$.
Let's see what this means for the horizontal accuracy of $alpha(y)$ and $beta(y)$. We can write
$
alpha(y) = alpha(y_0) + (y - y_0) / (y_1 - y_0) (alpha(y_1) - alpha(y_0)),
$
and similarly for $beta(y)$. Combining these and recalling our definitions of $c$ and $d$,
$
alpha(y) - beta(y) = -c + (y - y_0) / (y_1 - y_0) d.
$
For the true value of $y$, this is zero of course. With our error bound of $(2 epsilon_0 + epsilon_t) |y_1 - y_0| + epsilon_0 M$ on our
approximation of $y$ (let's call it $hat(y)$),
$
abs(alpha(hat(y)) - beta(hat(y))) <= (2 epsilon_0 + epsilon_t) d + (epsilon_0 M) / (y_1 - y_0) d
<= epsilon_t d + 3 epsilon_0 (M d) / (y_1 - y_0).
$
If we assume that $16 (epsilon + 2 epsilon_0 M) / d <= 3$ then $epsilon_t d <= 4 epsilon + 8 epsilon_0 M$.
Since $d/(y_1 - y_1)$ is the difference of the inverse-slopes of $alpha$ and $beta$, it's bounded by twice
the maximum of these inverse-slopes.
]
Putting all this together (and handling the "chamfers" yet):
#lemma[
Assume all line segments are inside the square $[-M/2, M/2]^2$,
and take $delta = 64 epsilon_0 M$. If $epsilon_0 <= 1/4$ then
- We can compute $x$ coordinates of line segments to additive accuracy $delta/8$.
- If $alpha$ $(3 delta)/4$-crosses $beta$, we can compute a crossing height $hat(y)$ such
that
$
|alpha(hat(y)) - beta(hat(y))| <= (9 delta) /16 (1 + max(1/m_alpha, 1/m_beta)).
$
]
#proof[
We compute $x$ coordinates by first computing $t = (y - y_0) / (y_1 - y_0)$ (with error at most $3 epsilon_0 + 4 epsilon_0^2$ by
@lem-ratio-error). Then we apply @lem-convex-combination-error with $epsilon = 3 epsilon_0 + 4 epsilon_0^2$ to see that we
can compute the horizontal coordinate with error at most $(6 epsilon_0 + 4 epsilon_0^2)M$. If $epsilon_0 <= 1/4$ as we assumed,
this is at most $7 epsilon_0 M <= delta/8$. This proves the first claim.
For the second claim, we apply @lem-intersection-height-error with $epsilon = (6 epsilon_0 + 4 epsilon_0^2) M$.
With this $epsilon$, $epsilon + 2 epsilon_0 M) <= 9 epsilon_0 M$ and so $16/3 (epsilon + 2 epsilon_0 M) <= 48 epsilon_0 M = (3 delta)/4$.
Thus, if $alpha$ $(3 delta)/4$-crosses $beta$ then the assumptions of @lem-intersection-height-error are satisfied
(and in the conclusion, $4 (epsilon + 2 epsilon_0) M <= 36 epsilon_0 M = (9 delta) /16 $.
]
= Curves etc.
Let's move on from lines and talk about curves. Each curve is of the form $alpha: [0, 1] -> bb(R)^2$, which we'll sometimes
in components as $alpha_0: [0, 1] -> bb(R)$ and $alpha_1: [0, 1] -> bb(R)$. We'll assume that each $alpha_0$ is a strictly
increasing function. Write $S = [-1, 1] times [-1, 1]$ for the unit square in $bb(R)^2$. For a curve $alpha$ and a set $B subset bb(R)^2$, we write
$alpha + B$ for the set
$
alpha + B = {(x, y) in bb(R)^2 : exists t in [0, 1] "with" (x, y) - alpha(t) in B}
$
In other words, $alpha + B$ is the Minkowski sum between $B$ and the locus of $alpha$. For any set $B subset bb(R)^2$,
let
$
B_y = {x in bb(R) : (x, y) in B}.
$
For example, here is a picture of $(alpha + epsilon B)_y$: (TODO)
== The curve orders
When moving to curves, it seems useful to work with more abstract orders. For fixed $epsilon$ and $y$, we'll define
three of them: the "weak pre-order," the "strong order," and the "weak order."
#let weakprelt = sym.lt.tri
#let weakpreleq = sym.lt.eq.tri
#let notweakprelt = sym.lt.tri.not
#let weakpreeq = sym.eq.dots
#let weaklt = sym.lt
#let stronglt = sym.lt.double
#let weaklt = sym.lt
#let weakeq = sym.tilde
We begin with the weak pre-order.
#def[
For each $epsilon >= 0$ and $y in bb(R)$, let $weakprelt_(y,epsilon)$ be a relation satisfying:
- if $alpha weakprelt_(y,epsilon) beta$ then $(alpha + epsilon/2 S)_y <= (beta + epsilon/2 S)_y$,
- if $(alpha + epsilon S)_y <= (beta + epsilon S)_y$ then $alpha weakprelt_(y,epsilon) beta$,
- for any $k >= 1$, there are no $alpha_1, ..., alpha_k$ with $alpha_1 weakprelt_(y,epsilon) alpha_2 weakprelt_(y,epsilon) dots.h.c weakprelt_(y,epsilon) alpha_k weakprelt_(y,epsilon) alpha_1$.
We call this last condition the "no cycles" condition.
]
Note that we *do not* assume transitivity: $alpha weakprelt_(y,epsilon) beta
weakprelt_(y,epsilon) gamma$ does not imply that $alpha weakprelt_(y,epsilon)
gamma$. We also do not assume anti-symmetry: it's possible to have neither
$alpha weakprelt_(y,epsilon) beta$ nor $beta weakprelt_(y,epsilon) alpha$. In
fact, if $(alpha + epsilon/2)_y$ and $(beta + epsilon/2 S)_y$ overlap, it is
guaranteed that neither of these orderings hold.
#def[
If neither $alpha weakprelt_(y,epsilon) beta$ nor $beta weakprelt_(y,epsilon) alpha$, we say that
$
alpha weakpreeq_(y,epsilon) beta.
$
If either $alpha weakprelt_(y,epsilon) beta$ or
$alpha weakpreeq_(y,epsilon) beta$, we write
$alpha weakpreleq_(y,epsilon) beta$. Equivalently,
$alpha weakpreleq_(y,epsilon) beta$ is the negation of
$beta weakprelt_(y,epsilon) alpha$.
]
TODO: draw a picture.
The strong order is similar to the weak pre-order, but with bigger epsilons.
#def[
For each $epsilon >= 0$ and $y in bb(R)$, let $stronglt_(y,epsilon)$ be a relation satisfying:
- if $alpha stronglt_(y,epsilon) beta$ then $(alpha + 2 epsilon S)_y <= (beta + 2 epsilon S)_y$,
- if $(alpha + 3 epsilon S)_y <= (beta + 3 epsilon S)_y$ then $alpha stronglt_(y,epsilon) beta$,
- for any $k >= 1$, there are no $alpha_1, ..., alpha_k$ with $alpha_1 stronglt_(y,epsilon) alpha_2 stronglt_(y,epsilon) dots.h.c stronglt_(y,epsilon) alpha_k stronglt_(y,epsilon) alpha_1$.
]
The definition of the weak order is a little strange at first. The initial idea
is that we want to ensure that our sweep-line is ordered by the weak pre-order:
we are going to try to ensure that if our sweep-line at $y$ is $alpha^1, ..., alpha^n$
then for all $i < j$, $alpha^i weakpreleq_y alpha^j$. Or in other words, we want to
ensure that whenever $alpha^j$ is definitely to the right of $alpha^i$, it comes after $alpha^i$
in the sweep-line.
For sweep-line algorithms to be efficient, we need to avoid too many segment comparisons.
In the classical Bentley-Ottmann algorithm with exact arithmetic this is easy: whenever
we look for intersections against segment $alpha^i$, we compare it with its sweep-line
neighbors to the left and right (so, $alpha^(i-1)$ and $alpha^(i+1)$ in our notation).
Inexact arithmetic will make this a little trickier; we will typically need to look further
than just the immediate neighbors of $alpha^i$. However, we will need *some* mechanism
for early stopping, some way to say that after we have compared $alpha^i$ to, say, $alpha^(i-k)$
through $alpha^(i-1)$, we can be sure that no other $alpha^j$ (for $j < i - k$) needs
to be checked. The intuitive idea is that if $alpha^(i-k)$ is very far to the left of $alpha^i$
then we can stop, because then $alpha^(i-k)$ will "shield" $alpha^i$ from segments further
left, in much the same way that its immediate neighbors "shield" $alpha^i$ in the classical
sweep-line algorithm.
We would like to use the strong order $stronglt_y$ to check whether $alpha^(i-k)$ is "very far
to the left" of $alpha^i$, but we run into trouble with examples like this:
TODO: picture.
Here, we clearly see that $alpha$ is to the left of $beta$; in situations like this, if we're
examining $beta$ for potential intersections and we see $alpha$ to its left, we'd really like
to stop looking further left. But $gamma$ is a problem: it's almost horizontal, and the
sweep-line at $y$ is at an ambiguous height, being more than $epsilon / 2$ but less than $epsilon$ below $gamma$.
According to the definition of our weak pre-order,
$alpha weakprelt_y gamma$ and $alpha weakpreeq_y gamma$ are
both allowed, as are both
$beta weakprelt_y gamma$ and $beta weakpreeq_y gamma$. So imagine that $alpha weakpreeq_y gamma$ and $beta weakprelt_y gamma$.
Before adding $beta$, the sweep-line could have been $(gamma, alpha)$. Then we could have tried to add $beta$ after $alpha$
(because clearly $beta$ is to the right of $alpha$), leading to the sweep-line $(gamma, alpha, beta)$.
Scanning for any intersections with the newly-added $beta$ won't find any because we'll look to the left, see
$alpha$, and stop. The result of all this is the illegal sweep-line $(gamma, alpha, beta)$, in which
$beta weakprelt_y gamma$ but $gamma$ comes first in the sweep-line.
Basically, it seems hard to have both early stopping (which we want for efficiency) and the
ordering invariants that we need for correctness. Our "fix" is to change the definition of
our ordering (while still keeping it tight enough for the correctness guarantees that we need).
It looks almost like cheating, because we're going to redefine it precisely so that our early
stopping will work.
#def[
Let $cal(S)_y$ be the set of all segments whose vertical range contains $y$. Say that $alpha weaklt_y beta$
if $alpha weakprelt_y beta$ and there do not exist $alpha', beta' in cal(S)_y$ with $beta weakpreleq_y beta' stronglt alpha' weakpreleq_y alpha$.
]<def-weak-order>
In other words, we define $weaklt$ to be the same as $weakprelt$, but if there's ever a situation where the (not-yet-specified)
early stopping algorithm could miss a wrongly ordered pair $alpha weakprelt beta$ by witnessing a gap between $beta'$ and $alpha'$,
then we weaken the $weakprelt$ order and declare instead that $alpha weakeq beta$ instead of $alpha weaklt beta$.
To see how this solves our tricky example above, note that when instantiating @def-weak-order with $alpha = beta$, $beta = gamma$,
$beta' = alpha$, and $alpha' = beta$, we see that there *do* exist $alpha', beta' in cal(S)_y$ with the required properties,
and so in fact $beta$ and $gamma$ are not strictly ordered in the weak order $weaklt$ even though they were strictly ordered
in the weak pre-order $weakprelt$. In particular, the sweep-line $(gamma, alpha, beta)$ is legal with respect to the weak order $weaklt$.
You might object to @def-weak-order because of how hilariously inefficient it will be to implement. The
clever part, though, is that we won't need to implement it. Our algorithm will make its decisions based
only on the weak pre-order $weakprelt$ and the strong order $stronglt$. The inefficient weak order $weaklt$
will only be used in the correctness proof.
+342
View File
@@ -0,0 +1,342 @@
use std::path::PathBuf;
use anyhow::anyhow;
use clap::{Args, Parser};
use kurbo::BezPath;
use skrifa::{FontRef, MetadataProvider};
use svg::Document;
use linesweeper::{
Point, generators,
topology::{BinaryWindingNumber, Topology},
};
use linesweeper_util::{outline_to_bezpath, svg_to_bezpaths};
#[derive(Copy, Clone, Debug)]
enum Op {
Union,
Intersection,
Xor,
Difference,
ReverseDifference,
}
#[derive(Copy, Clone, Debug, clap::ValueEnum)]
enum Example {
Checkerboard,
SlantedCheckerboard,
Slanties,
Star,
}
#[derive(Parser)]
struct Cli {
#[arg(long)]
output: PathBuf,
#[command(flatten)]
input: Input,
#[arg(long)]
non_zero: bool,
#[arg(long)]
path_bool: bool,
#[arg(long)]
epsilon: Option<f64>,
}
#[derive(Args, Debug)]
#[group(required = true, multiple = false)]
struct Input {
input: Option<PathBuf>,
#[arg(long)]
example: Option<Example>,
#[arg(long)]
char: Option<Vec<char>>,
}
fn points_to_bez(ps: Vec<Point>) -> BezPath {
let mut ps = ps.into_iter();
let mut ret = BezPath::new();
if let Some(p) = ps.next() {
ret.move_to((p.x, p.y));
}
for p in ps {
ret.line_to((p.x, p.y));
}
ret.close_path();
ret
}
fn contours_to_bezs((cs0, cs1): (Vec<Vec<Point>>, Vec<Vec<Point>>)) -> (BezPath, BezPath) {
(
cs0.into_iter().flat_map(points_to_bez).collect(),
cs1.into_iter().flat_map(points_to_bez).collect(),
)
}
fn get_contours(input: &Input) -> anyhow::Result<(BezPath, BezPath)> {
match (&input.input, &input.example, &input.char) {
(Some(path), None, None) => {
let input = std::fs::read_to_string(path)?;
let tree = usvg::Tree::from_str(&input, &usvg::Options::default())?;
let mut contours = svg_to_bezpaths(&tree).into_iter();
let first = contours.next().unwrap();
let rest = contours.flatten().collect();
Ok((first, rest))
}
(None, Some(example), None) => match example {
Example::Checkerboard => Ok(contours_to_bezs(generators::checkerboard(10))),
Example::SlantedCheckerboard => {
Ok(contours_to_bezs(generators::slanted_checkerboard(10)))
}
Example::Slanties => Ok(contours_to_bezs(generators::slanties(10))),
Example::Star => Ok(generators::star(20)),
},
(None, None, Some(chars)) => {
let ws = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let font_path = format!("{ws}/tests/fonts/Inconsolata-Regular.ttf");
let data = std::fs::read(&font_path).unwrap();
let font_ref = FontRef::new(&data).unwrap();
let charmap = font_ref.charmap();
let paths = chars
.iter()
.map(|c| -> anyhow::Result<_> {
let id = charmap.map(*c).ok_or(anyhow!("{c} not in the charmap"))?;
let outline = font_ref
.outline_glyphs()
.get(id)
.ok_or(anyhow!("missing glyph for {c}"))?;
Ok(outline_to_bezpath(outline))
})
.collect::<Result<Vec<_>, _>>()?;
Ok((paths[0].clone(), paths[1..].iter().flatten().collect()))
}
_ => unreachable!(),
}
}
pub fn main() -> anyhow::Result<()> {
let args = Cli::parse();
let (shape_a, shape_b) = get_contours(&args.input)?;
let eps = args.epsilon.unwrap_or(0.1);
let top = Topology::from_paths_binary(&shape_a, &shape_b, eps)?;
let bbox = top.bounding_box();
let min_x = bbox.min_x();
let min_y = bbox.min_y();
let max_x = bbox.max_x();
let max_y = bbox.max_y();
let pad = 1.0 + eps;
let one_width = max_x - min_x + 2.0 * pad;
let one_height = max_y - min_y + 2.0 * pad;
let stroke_width = (max_y - min_y).max(max_x - max_y) / 512.0;
let mut document = svg::Document::new().set(
"viewBox",
(min_x - pad, min_y - pad, one_width * 3.0, one_height * 2.0),
);
// Draw the original document.
for c in [&shape_a, &shape_b] {
let mut data = svg::node::element::path::Data::new();
for el in c {
let p = |point: kurbo::Point| (point.x, point.y);
data = match el {
kurbo::PathEl::MoveTo(p0) => data.move_to(p(p0)),
kurbo::PathEl::LineTo(p0) => data.line_to(p(p0)),
kurbo::PathEl::QuadTo(p0, p1) => data.quadratic_curve_to((p(p0), p(p1))),
kurbo::PathEl::CurveTo(p0, p1, p2) => data.cubic_curve_to((p(p0), p(p1), p(p2))),
kurbo::PathEl::ClosePath => data.close(),
};
}
let path = svg::node::element::Path::new()
.set("stroke", "black")
.set("stroke-width", stroke_width)
.set("stroke-linecap", "round")
.set("stroke-linejoin", "round")
.set("opacity", 0.2)
.set("fill", "none")
.set("d", data);
document = document.add(path);
}
let add_one = |doc, op, x_off, y_off| {
if args.path_bool {
add_path_bool_op(doc, op, &shape_a, &shape_b, x_off, y_off, stroke_width)
} else {
add_op(doc, op, args.non_zero, &top, x_off, y_off, stroke_width)
}
};
document = add_one(document, Op::Union, one_width, 0.0);
document = add_one(document, Op::Intersection, one_width * 2.0, 0.0);
document = add_one(document, Op::Xor, 0.0, one_height);
document = add_one(document, Op::Difference, one_width, one_height);
document = add_one(document, Op::ReverseDifference, one_width * 2.0, one_height);
svg::save(&args.output, &document)?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn add_op(
mut doc: Document,
op: Op,
non_zero: bool,
top: &Topology<BinaryWindingNumber>,
x_off: f64,
y_off: f64,
stroke_width: f64,
) -> Document {
let contours = top.contours(|w| {
let inside = |winding| {
if non_zero {
winding != 0
} else {
winding % 2 != 0
}
};
match op {
Op::Union => inside(w.shape_a) || inside(w.shape_b),
Op::Intersection => inside(w.shape_a) && inside(w.shape_b),
Op::Xor => inside(w.shape_a) != inside(w.shape_b),
Op::Difference => inside(w.shape_a) && !inside(w.shape_b),
Op::ReverseDifference => inside(w.shape_b) && !inside(w.shape_a),
}
});
let colors = [
"#005F73", "#0A9396", "#94D2BD", "#E9D8A6", "#EE9B00", "#CA6702", "#BB3E03", "#AE2012",
"#9B2226",
];
let mut color_idx = 0;
for group in contours.grouped() {
let mut data = svg::node::element::path::Data::new();
for contour_idx in group {
let path = &contours[contour_idx].path;
for el in path.iter() {
data = match el {
kurbo::PathEl::MoveTo(p) => data.move_to((p.x + x_off, p.y + y_off)),
kurbo::PathEl::LineTo(p) => data.line_to((p.x + x_off, p.y + y_off)),
kurbo::PathEl::QuadTo(p0, p1) => data.quadratic_curve_to((
(p0.x + x_off, p0.y + y_off),
(p1.x + x_off, p1.y + y_off),
)),
kurbo::PathEl::CurveTo(p0, p1, p2) => data.cubic_curve_to((
(p0.x + x_off, p0.y + y_off),
(p1.x + x_off, p1.y + y_off),
(p2.x + x_off, p2.y + y_off),
)),
kurbo::PathEl::ClosePath => data.close(),
};
}
data = data.close();
}
let path = svg::node::element::Path::new()
.set("d", data)
.set("stroke", "black")
.set("stroke-width", stroke_width)
.set("stroke-linecap", "round")
.set("stroke-linejoin", "round")
.set("fill", colors[color_idx]);
doc = doc.add(path);
color_idx = (color_idx + 1) % colors.len();
}
doc
}
fn bezpath_to_path_bool(p: &BezPath) -> Vec<path_bool::PathSegment> {
let mut ret = Vec::new();
for seg in p.segments() {
let dv = |p: kurbo::Point| glam::DVec2::from((p.x, p.y));
let seg = match seg {
kurbo::PathSeg::Line(line) => path_bool::PathSegment::Line(dv(line.p0), dv(line.p1)),
kurbo::PathSeg::Quad(q) => {
path_bool::PathSegment::Quadratic(dv(q.p0), dv(q.p1), dv(q.p2))
}
kurbo::PathSeg::Cubic(c) => {
path_bool::PathSegment::Cubic(dv(c.p0), dv(c.p1), dv(c.p2), dv(c.p3))
}
};
ret.push(seg);
}
ret
}
#[allow(clippy::too_many_arguments)]
fn add_path_bool_op(
mut doc: Document,
op: Op,
shape_a: &BezPath,
shape_b: &BezPath,
x_off: f64,
y_off: f64,
stroke_width: f64,
) -> Document {
let mut shape_a = bezpath_to_path_bool(shape_a);
let mut shape_b = bezpath_to_path_bool(shape_b);
let fill = path_bool::FillRule::EvenOdd;
let op = match op {
Op::Union => path_bool::PathBooleanOperation::Union,
Op::Intersection => path_bool::PathBooleanOperation::Intersection,
Op::Xor => path_bool::PathBooleanOperation::Exclusion,
Op::Difference => path_bool::PathBooleanOperation::Difference,
Op::ReverseDifference => {
std::mem::swap(&mut shape_a, &mut shape_b);
path_bool::PathBooleanOperation::Difference
}
};
let result = path_bool::path_boolean(&shape_a, fill, &shape_b, fill, op).unwrap();
let colors = [
"#005F73", "#0A9396", "#94D2BD", "#E9D8A6", "#EE9B00", "#CA6702", "#BB3E03", "#AE2012",
"#9B2226",
];
let mut color_idx = 0;
for path in result {
let mut data = svg::node::element::path::Data::new();
if path.is_empty() {
continue;
}
let start = path[0].start();
data = data.move_to((start.x + x_off, start.y + y_off));
for seg in path {
data = match seg {
path_bool::PathSegment::Line(_, p) => data.line_to((p.x + x_off, p.y + y_off)),
path_bool::PathSegment::Quadratic(_, p0, p1) => data.quadratic_curve_to((
(p0.x + x_off, p0.y + y_off),
(p1.x + x_off, p1.y + y_off),
)),
path_bool::PathSegment::Cubic(_, p0, p1, p2) => data.cubic_curve_to((
(p0.x + x_off, p0.y + y_off),
(p1.x + x_off, p1.y + y_off),
(p2.x + x_off, p2.y + y_off),
)),
path_bool::PathSegment::Arc(..) => unimplemented!(),
};
}
data = data.close();
let path = svg::node::element::Path::new()
.set("d", data)
.set("stroke", "black")
.set("stroke-width", stroke_width)
.set("stroke-linecap", "round")
.set("stroke-linejoin", "round")
.set("fill", colors[color_idx]);
doc = doc.add(path);
color_idx = (color_idx + 1) % colors.len();
}
doc
}
+144
View File
@@ -0,0 +1,144 @@
use kurbo::{Affine, BezPath};
use linesweeper::topology::{Topology, WindingNumber};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
struct Windings {
main: i32,
cutout: i32,
modifier: i32,
}
impl std::ops::Add for Windings {
type Output = Windings;
fn add(self, rhs: Self) -> Self::Output {
Self {
main: self.main + rhs.main,
cutout: self.cutout + rhs.cutout,
modifier: self.modifier + rhs.modifier,
}
}
}
impl std::ops::AddAssign for Windings {
fn add_assign(&mut self, rhs: Self) {
*self = *self + rhs
}
}
impl WindingNumber for Windings {
type Tag = Tag;
fn single(tag: Self::Tag, positive: bool) -> Self {
let sign = if positive { 1 } else { -1 };
match tag {
Tag::Main => Self {
main: sign,
..Default::default()
},
Tag::Cutout => Self {
cutout: sign,
..Default::default()
},
Tag::Modifier => Self {
modifier: sign,
..Default::default()
},
}
}
fn of_tag(&self, tag: Self::Tag) -> Self {
match tag {
Tag::Main => Self {
main: self.main,
..Default::default()
},
Tag::Cutout => Self {
cutout: self.cutout,
..Default::default()
},
Tag::Modifier => Self {
modifier: self.modifier,
..Default::default()
},
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum Tag {
Main,
Cutout,
Modifier,
}
pub fn main() -> anyhow::Result<()> {
let bus = "M0,80 C0,35.8 35.8,0 80,0 L432,0 C476.2,0 512,35.8 512,80 L512,368 C512,394.2 499.4,417.4 480,432 L480,480 C480,497.7 465.7,512 448,512 L416,512 C398.3,512 384,497.7 384,480 L384,448 L128,448 L128,480 C128,497.7 113.7,512 96,512 L64,512 C46.3,512 32,497.7 32,480 L32,432 C12.6,417.4 0,394.2 0,368 L0,80 Z M129.9,152.2 L112,224 L400,224 L382.1,152.2 C378.5,138 365.7,128 351,128 L161,128 C146.3,128 133.5,138 130,152.2 Z M128,320 A32 32 0 1 0 64,320 A32 32 0 1 0 128,320 Z M416,352 A32 32 0 1 0 416,288 A32 32 0 1 0 416,352 Z";
let cutout = "M320 512V266.8C288.1 221.6 235.5 192 176 192C78.8 192 0 270.8 0 368c0 59.5 29.6 112.1 74.8 144H320z";
let modifier = "M144 512c79.5 0 144-64.5 144-144s-64.5-144-144-144S0 288.5 0 368s64.5 144 144 144zm67.3-164.7l-72 72c-6.2 6.2-16.4 6.2-22.6 0l-40-40c-6.2-6.2-6.2-16.4 0-22.6s16.4-6.2 22.6 0L128 385.4l60.7-60.7c6.2-6.2 16.4-6.2 22.6 0s6.2 16.4 0 22.6z";
let bus = BezPath::from_svg(bus).unwrap();
let cutout = Affine::translate((193.0, 0.0)) * BezPath::from_svg(cutout).unwrap();
let modifier = Affine::translate((224.0, 0.0)) * BezPath::from_svg(modifier).unwrap();
let eps = 1e-5;
let top = Topology::<Windings>::from_paths(
[
(&bus, Tag::Main),
(&cutout, Tag::Cutout),
(&modifier, Tag::Modifier),
],
eps,
)
.unwrap();
let bbox = top.bounding_box();
let min_x = bbox.min_x();
let min_y = bbox.min_y();
let max_x = bbox.max_x();
let max_y = bbox.max_y();
let pad = 1.0 + eps;
let one_width = max_x - min_x + 2.0 * pad;
let one_height = max_y - min_y + 2.0 * pad;
let mut doc = svg::Document::new().set(
"viewBox",
(min_x - pad, min_y - pad, one_width * 3.0, one_height * 2.0),
);
let contours = top.contours(|w| {
let inside = |winding| winding != 0;
(inside(w.main) && !inside(w.cutout)) || inside(w.modifier)
});
for group in contours.grouped() {
let mut data = svg::node::element::path::Data::new();
for contour_idx in group {
let path = &contours[contour_idx].path;
for el in path.iter() {
data = match el {
kurbo::PathEl::MoveTo(p) => data.move_to((p.x, p.y)),
kurbo::PathEl::LineTo(p) => data.line_to((p.x, p.y)),
kurbo::PathEl::QuadTo(p0, p1) => {
data.quadratic_curve_to(((p0.x, p0.y), (p1.x, p1.y)))
}
kurbo::PathEl::CurveTo(p0, p1, p2) => {
data.cubic_curve_to(((p0.x, p0.y), (p1.x, p1.y), (p2.x, p2.y)))
}
kurbo::PathEl::ClosePath => data.close(),
};
}
}
let path = svg::node::element::Path::new()
.set("d", data)
.set("stroke", "black")
.set("stroke-linecap", "round")
.set("stroke-linejoin", "round");
doc = doc.add(path);
}
svg::save("out.svg", &doc)?;
Ok(())
}
+130
View File
@@ -0,0 +1,130 @@
use color::ColorSpace as _;
use kurbo::{Affine, BezPath, CubicBez, PathSeg, Point};
use linesweeper::curve::{Order, intersect_cubics, y_subsegment};
fn flip(c: CubicBez) -> CubicBez {
Affine::reflect(Point::ZERO, (0.0, 1.0)) * c
}
fn shift(c: CubicBez, dx: f64) -> CubicBez {
Affine::translate((0.0, dx)) * c
}
fn horiz_at_midpoint() -> CubicBez {
CubicBez::new((-1.0, -1.0), (-1.0, 1.0), (1.0, -1.0), (1.0, 1.0))
}
fn c() -> CubicBez {
CubicBez::new((1.0, -1.0), (-1.0, -1.0), (-1.0, 1.0), (1.0, 1.0))
}
fn diagonal() -> CubicBez {
CubicBez::new(
(-1.0, -1.0),
(-1.0 / 3.0, -1.0 / 3.0),
(1.0 / 3.0, 1.0 / 3.0),
(1.0, 1.0),
)
}
fn vertical() -> CubicBez {
CubicBez::new((0.0, -1.0), (0.0, -1.0 / 3.0), (0.0, 1.0 / 3.0), (0.0, 1.0))
}
fn to_svg(c: CubicBez) -> String {
BezPath::from_path_segments([PathSeg::Cubic(c)].into_iter()).to_svg()
}
fn depth_color(depth: f64) -> String {
let start = 200.0;
let end = 382.0;
let [r, g, b] = color::Srgb::clip(color::Oklch::convert::<color::Srgb>([
0.6,
0.3,
start + (end - start) * depth as f32,
]));
let c = |x: f32| (x * 255.0).round() as u8;
format!("#{:02x}{:02x}{:02x}", c(r), c(g), c(b))
}
fn compare_curves(
c0: CubicBez,
c1: CubicBez,
eps: f64,
max_depth: u32,
) -> svg::node::element::Group {
let mut g = svg::node::element::Group::new();
let cmp = intersect_cubics(c0, c1, 0.5 * eps, eps);
g = g.add(
svg::node::element::Path::new()
.set("d", to_svg(c1))
.set("fill", "none")
.set("stroke", "black")
.set("stroke-width", 0.01)
.set("stroke-linecap", "round")
.set("stroke-linejoin", "round"),
);
for (start, end, order) in cmp.iter() {
let color = match order {
Order::Right => "green",
Order::Ish => "red",
Order::Left => "blue",
};
let c = y_subsegment(c0, start, end);
g = g.add(
svg::node::element::Path::new()
.set("d", to_svg(c))
.set("fill", "none")
.set("stroke", color)
.set("stroke-width", 0.01)
.set("stroke-linecap", "round")
.set("stroke-linejoin", "round"),
);
}
let mut start = -1.0;
for m in &cmp.metrics.entries {
if m.depth == 0 {
start = m.end;
continue;
}
let w = (m.depth as f64 / max_depth as f64).clamp(0.01, 1.0);
let h = (m.end - start).max(0.01);
g = g.add(
svg::node::element::Rectangle::new()
.set("width", w)
.set("height", h)
.set("x", 1.5)
.set("y", start)
.set("fill", depth_color(w)),
);
start = m.end;
}
g
}
pub fn main() {
let curves = [
(diagonal(), flip(diagonal())),
(diagonal(), shift(diagonal(), 0.01)),
(c(), vertical()),
(c(), diagonal()),
(c(), flip(c())),
(c(), shift(c(), 0.01)),
(horiz_at_midpoint(), diagonal()),
(horiz_at_midpoint(), c()),
(horiz_at_midpoint(), flip(horiz_at_midpoint())),
(horiz_at_midpoint(), shift(horiz_at_midpoint(), 0.01)),
];
let mut doc =
svg::Document::new().set("viewBox", (0.0, 0.0, 5.5, 1.0 + 2.5 * curves.len() as f64));
for (i, &(c0, c1)) in curves.iter().enumerate() {
let y = i as f64 * 2.5 + 1.5;
let g = compare_curves(c0, c1, 1e-6, 6).set("transform", format!("translate(0, {y})",));
doc = doc.add(g);
}
svg::save("out.svg", &doc).unwrap();
}
+130
View File
@@ -0,0 +1,130 @@
use std::path::PathBuf;
use anyhow::{anyhow, bail};
use clap::Parser;
use kurbo::BezPath;
use svg::Document;
use linesweeper::{
BinaryOp, FillRule, binary_op,
topology::{Contours, Topology},
};
#[derive(Parser)]
struct Cli {
#[arg(long)]
output: PathBuf,
#[arg(long)]
input: PathBuf,
}
pub fn main() -> anyhow::Result<()> {
let args = Cli::parse();
let input = std::fs::read_to_string(&args.input)?;
let mut input_lines = input.lines();
let op = input_lines.next().ok_or(anyhow!("no op line"))?;
let op = match op {
"union" => BinaryOp::Union,
"intersection" => BinaryOp::Intersection,
"difference" => BinaryOp::Difference,
"xor" => BinaryOp::Xor,
_ => bail!("unknown op {op}"),
};
let shape_a = input_lines.next().ok_or(anyhow!("no first shape"))?;
let shape_b = input_lines.next().ok_or(anyhow!("no second shape"))?;
for extra_line in input_lines {
if !extra_line.trim().is_empty() {
eprintln!("ignoring extra line: {extra_line}");
}
}
let shape_a = BezPath::from_svg(shape_a)?;
let shape_b = BezPath::from_svg(shape_b)?;
let eps = 1e-5;
let contours = binary_op(&shape_a, &shape_b, FillRule::EvenOdd, op)?;
let top = Topology::from_paths_binary(&shape_a, &shape_b, eps)?;
let bbox = top.bounding_box();
let min_x = bbox.min_x();
let min_y = bbox.min_y();
let max_x = bbox.max_x();
let max_y = bbox.max_y();
let pad = 1.0 + eps;
let one_width = max_x - min_x + 2.0 * pad;
let one_height = max_y - min_y + 2.0 * pad;
let stroke_width = (max_y - min_y).max(max_x - max_y) / 512.0;
let mut document = svg::Document::new().set(
"viewBox",
(min_x - pad, min_y - pad, one_width * 2.0, one_height),
);
// Draw the original document.
for (c, color) in [(&shape_a, "green"), (&shape_b, "blue")] {
let mut data = svg::node::element::path::Data::new();
for el in c {
let p = |point: kurbo::Point| (point.x, point.y);
data = match el {
kurbo::PathEl::MoveTo(p0) => data.move_to(p(p0)),
kurbo::PathEl::LineTo(p0) => data.line_to(p(p0)),
kurbo::PathEl::QuadTo(p0, p1) => data.quadratic_curve_to((p(p0), p(p1))),
kurbo::PathEl::CurveTo(p0, p1, p2) => data.cubic_curve_to((p(p0), p(p1), p(p2))),
kurbo::PathEl::ClosePath => data.close(),
};
}
let path = svg::node::element::Path::new()
.set("stroke", "black")
.set("stroke-width", stroke_width)
.set("stroke-linecap", "round")
.set("stroke-linejoin", "round")
.set("opacity", 0.5)
.set("fill", color)
.set("d", data);
document = document.add(path);
}
document = add_op(document, &contours, one_width, stroke_width);
svg::save(&args.output, &document)?;
Ok(())
}
fn add_op(mut doc: Document, contours: &Contours, x_off: f64, stroke_width: f64) -> Document {
for group in contours.grouped() {
let mut data = svg::node::element::path::Data::new();
for contour_idx in group {
let path = &contours[contour_idx].path;
for el in path.iter() {
data = match el {
kurbo::PathEl::MoveTo(p) => data.move_to((p.x + x_off, p.y)),
kurbo::PathEl::LineTo(p) => data.line_to((p.x + x_off, p.y)),
kurbo::PathEl::QuadTo(p0, p1) => {
data.quadratic_curve_to(((p0.x + x_off, p0.y), (p1.x + x_off, p1.y)))
}
kurbo::PathEl::CurveTo(p0, p1, p2) => data.cubic_curve_to((
(p0.x + x_off, p0.y),
(p1.x + x_off, p1.y),
(p2.x + x_off, p2.y),
)),
kurbo::PathEl::ClosePath => data.close(),
};
}
data = data.close();
}
let path = svg::node::element::Path::new()
.set("d", data)
.set("stroke", "black")
.set("stroke-width", stroke_width)
.set("stroke-linecap", "round")
.set("stroke-linejoin", "round")
.set("fill", "pink");
doc = doc.add(path);
}
doc
}
+108
View File
@@ -0,0 +1,108 @@
use std::path::PathBuf;
use clap::Parser;
use linesweeper::topology::Topology;
use linesweeper_util::svg_to_bezpaths;
#[derive(Parser)]
struct Args {
input: PathBuf,
output: PathBuf,
#[arg(long)]
epsilon: Option<f64>,
}
pub fn main() -> anyhow::Result<()> {
let args = Args::parse();
let input = std::fs::read_to_string(&args.input)?;
let tree = usvg::Tree::from_str(&input, &usvg::Options::default())?;
let contours = svg_to_bezpaths(&tree);
let eps = args.epsilon.unwrap_or(0.1);
let top =
Topology::from_paths_binary(&contours[0], &contours[1..].iter().flatten().collect(), eps)?;
let bbox = top.bounding_box();
let min_x = bbox.min_x();
let min_y = bbox.min_y();
let max_x = bbox.max_x();
let max_y = bbox.max_y();
let pad = 8.0 + eps;
let stroke_width = (max_y - min_y).max(max_x - max_y) / 512.0;
let dot_radius = stroke_width * 1.5;
let mut document = svg::Document::new().set(
"viewBox",
(
min_x - pad,
min_y - pad,
max_x - min_x + 2.0 * pad,
max_y - min_y + 2.0 * pad,
),
);
let text_size = "8px";
for seg in top.segment_indices() {
let p0 = &top.point(seg.first_half());
let p1 = &top.point(seg.second_half());
let (x0, y0) = (p0.x, p0.y);
let (x1, y1) = (p1.x, p1.y);
let c = svg::node::element::Circle::new()
.set("r", dot_radius)
.set("cy", y0)
.set("cx", x0)
.set("opacity", 0.5)
.set("fill", "blue");
document = document.add(c);
let c = svg::node::element::Circle::new()
.set("r", dot_radius)
.set("cy", y1)
.set("cx", x1)
.set("opacity", 0.5)
.set("fill", "blue");
document = document.add(c);
let data = svg::node::element::path::Data::new()
.move_to((x0, y0))
.line_to((x1, y1));
let path = svg::node::element::Path::new()
.set("stroke", "black")
.set("stroke-width", stroke_width / 2.0)
.set("stroke-opacity", "0.5")
.set("d", data);
document = document.add(path);
let nx = y1 - y0;
let ny = x0 - x1;
let norm = ((nx * nx) + (ny * ny)).sqrt();
let nx = nx / norm * 3.0 * dot_radius;
let ny = ny / norm * 3.0 * dot_radius;
let text = svg::node::element::Text::new(format!(
"{:?}",
top.winding_counter_clockwise(seg.first_half())
))
.set("font-size", text_size)
.set("text-anchor", "start")
.set("x", (x0 + x1) / 2.0 + nx)
.set("y", (y0 + y1) / 2.0 + ny);
document = document.add(text);
let text =
svg::node::element::Text::new(format!("{:?}", top.winding_clockwise(seg.first_half())))
.set("font-size", text_size)
.set("text-anchor", "end")
.set("x", (x0 + x1) / 2.0 - nx)
.set("y", (y0 + y1) / 2.0 - ny);
document = document.add(text);
}
svg::save(&args.output, &document)?;
Ok(())
}
@@ -0,0 +1,8 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 87ffcdf96df9f88e13c595fd71e4c6e86eadf365efcec4c7f8f395c57fdd6f20 # shrinks to perturbations = [Subdivision { t: 0.8279203883070297, idx: 11865133298020245315, next: Base { idx: 0 } }]
cc e20fc6214f9e6b2d383bd5fe49c9c888a1b364ca32f9787f6e9427933f0ff66b # shrinks to perturbations = [Point { perturbation: PointPerturbation { x: Ulp(0), y: Ulp(1) }, idx: 11820034422209601546, next: Base { idx: 0 } }]
@@ -0,0 +1,7 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 9652115745f15b2b647d72abc389e36c8ee3562dfeab63f8075c9135696eac71 # shrinks to perturbations = [Point { perturbation: PointPerturbation { x: RationalPerturbation { eps: 0 }, y: RationalPerturbation { eps: 6687372532051531/72057594037927936 } }, idx: 466820082892447096, next: Base { idx: 0 } }, Point { perturbation: PointPerturbation { x: RationalPerturbation { eps: 0 }, y: RationalPerturbation { eps: 5420508530135135/576460752303423488 } }, idx: 14248258729692824517, next: Base { idx: 0 } }, Superimposition { left: Point { perturbation: PointPerturbation { x: RationalPerturbation { eps: -1501017540037737/18014398509481984 }, y: RationalPerturbation { eps: -8783504785369825/144115188075855872 } }, idx: 9515324635301739004, next: Base { idx: 0 } }, right: Base { idx: 0 } }]
+11
View File
@@ -0,0 +1,11 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc e06d16cadc32d2ad23df2c5f0b22509a990df8719857f47e2346e415d50443e5 # shrinks to perturbations = [Subdivision { t: 0.9394124935587298, idx: 2015950306058370793, next: Base { idx: 0 } }, Subdivision { t: 0.13141341476907323, idx: 16536748748586597932, next: Base { idx: 0 } }]
cc 66cc16b5c318ce64564d715666aa9130ee35f41ee43eb20c0f9a8cf770476ae3 # shrinks to perturbations = [Subdivision { t: 0.8093222325403372, idx: 11406037439407339269, next: Base { idx: 0 } }]
cc bf6883b0e72f307394f6942e6cca0c30d7ca5b1de20b66ebcb20c45904be4870 # shrinks to perturbations = [Point { perturbation: PointPerturbation { x: Ulp(0), y: Ulp(-1) }, idx: 13955836529066927736, next: Base { idx: 0 } }]
cc a9d1eacd0a3cf800ccfa5c5df4bf6aeba6295a40855cc8aa42a021801a1f31af # shrinks to perturbations = [Point { perturbation: PointPerturbation { x: Ulp(0), y: Ulp(1) }, idx: 12374941241701864599, next: Subdivision { t: 0.8636770242633537, idx: 14298434860461557715, next: Base { idx: 0 } } }, Base { idx: 0 }]
cc a70758452b3942fb51e74f97c3f255d77637a7b83402f2af7233dbdd53c417b3 # shrinks to perturbations = [Point { perturbation: PointPerturbation { x: Ulp(0), y: Ulp(-1) }, idx: 198564688037606045, next: Base { idx: 0 } }, Point { perturbation: PointPerturbation { x: Eps(-0.06666888378841745), y: Eps(-0.04843769070842934) }, idx: 13190014615537582403, next: Base { idx: 0 } }, Base { idx: 0 }]
+134
View File
@@ -0,0 +1,134 @@
//! Utilities for fuzz and/or property testing using `arbitrary`.
use arbitrary::Unstructured;
use kurbo::{CubicBez, Point};
use polycool::Quadratic;
use crate::geom;
/// Generate an arbitrary float in some range.
pub fn float_in_range(
start: f64,
end: f64,
u: &mut Unstructured<'_>,
) -> Result<f64, arbitrary::Error> {
if start >= end {
return Err(arbitrary::Error::EmptyChoose);
}
let num: u32 = u.arbitrary()?;
let t = num as f64 / u32::MAX as f64;
Ok((start + t * (end - start)).clamp(start, end))
}
fn float(u: &mut Unstructured<'_>) -> Result<f64, arbitrary::Error> {
float_in_range(-1e6, 1e6, u)
}
fn float_at_least(min: f64, u: &mut Unstructured<'_>) -> Result<f64, arbitrary::Error> {
float_in_range(min, 1e6, u)
}
fn float_at_most(max: f64, u: &mut Unstructured<'_>) -> Result<f64, arbitrary::Error> {
float_in_range(-1e6, max, u)
}
/// Generate a float in some range, but give it a chance to be close to another float.
fn another_float_in_range(
orig: f64,
start: f64,
end: f64,
u: &mut Unstructured<'_>,
) -> Result<f64, arbitrary::Error> {
let close: bool = u.arbitrary()?;
if close {
let ulps: i32 = u.int_in_range(-32..=32)?;
let scale = 1.0f64 + ulps as f64 * f64::EPSILON;
Ok((orig * scale).clamp(start, end))
} else {
float_in_range(start, end, u)
}
}
fn point(u: &mut Unstructured<'_>) -> Result<Point, arbitrary::Error> {
Ok(Point::new(float(u)?, float(u)?))
}
fn point_at_least(min: f64, u: &mut Unstructured<'_>) -> Result<Point, arbitrary::Error> {
Ok(Point::new(float(u)?, float_at_least(min, u)?))
}
fn point_at_most(max: f64, u: &mut Unstructured<'_>) -> Result<Point, arbitrary::Error> {
Ok(Point::new(float(u)?, float_at_most(max, u)?))
}
/// Generate an arbitrary cubic Bezier, guaranteed to be monotonically increasing in y.
pub fn monotonic_bezier(u: &mut Unstructured<'_>) -> Result<CubicBez, arbitrary::Error> {
let p0 = point(u)?;
let p3 = point(u)?;
let (p0, mut p3) = if p0.y < p3.y { (p0, p3) } else { (p3, p0) };
if p3.y == p0.y {
p3.y += 1.0;
}
let p1 = point_at_least(p0.y, u)?;
let p2 = point_at_most(p3.y, u)?;
let ret = CubicBez::new(p0, p1, p2, p3);
Ok(geom::monotonic_cubic_pieces(ret)
.into_iter()
.find(|x| x.piece.end().y > x.piece.start().y)
.unwrap()
.piece
.to_kurbo_cubic())
}
/// Generate an arbitrary cubic Bezier, guaranteed to be monotonically increasing in y.
///
/// This generated Bezier has a chance to be "close" to `first`, for example by starting
/// at the same point or with the same tangent.
pub fn another_monotonic_bezier(
u: &mut Unstructured<'_>,
first: &CubicBez,
) -> Result<CubicBez, arbitrary::Error> {
let same_start: bool = u.arbitrary()?;
let same_start_tangent: bool = same_start && u.arbitrary()?;
let p0 = if same_start { first.p0 } else { point(u)? };
let p1 = if same_start_tangent {
first.p1
} else {
point_at_least(p0.y, u)?
};
let p3 = point_at_least(p0.y.max(first.p0.y), u)?;
let p2 = point_at_most(p3.y, u)?;
let ret = CubicBez::new(p0, p1, p2, p3);
Ok(geom::monotonic_cubic_pieces(ret)
.into_iter()
.find(|x| x.piece.end().y > x.piece.start().y)
.map(|c| c.piece.to_kurbo_cubic())
.unwrap_or(ret))
}
/// Generate an arbitrary quadratic function, with coefficients of roughly the scale `size`.
pub fn quadratic(size: f64, u: &mut Unstructured<'_>) -> Result<Quadratic, arbitrary::Error> {
let use_coeffs: bool = u.arbitrary()?;
if use_coeffs {
let c2 = float_in_range(-size, size, u)?;
let c1 = another_float_in_range(c2, -size, size, u)?;
let c0 = another_float_in_range(c1, -size, size, u)?;
Ok(Quadratic::new([c0, c1, c2]))
} else {
// Generate the roots, with a bias towards an almost-repeated root.
let size = size.sqrt();
let r1 = float_in_range(-size, size, u)?;
let r2 = another_float_in_range(r1, -size, size, u)?;
let scale = float_in_range(-size, size, u)?;
Ok(Quadratic::new([scale * r1 * r2, -scale * (r1 + r2), scale]))
}
}
+159
View File
@@ -0,0 +1,159 @@
//! Special-case curve-comparisons for straight lines.
use kurbo::Line;
use crate::curve::{CurveOrder, MetricsStatus, Order};
fn line_at_ys(line: Line, y0: f64, y1: f64) -> (f64, f64) {
let dy_recip = (line.p1.y - line.p0.y).recip();
let t0 = (y0 - line.p0.y) * dy_recip;
let t1 = (y1 - line.p0.y) * dy_recip;
(
(1.0 - t0) * line.p0.x + t0 * line.p1.x,
(1.0 - t1) * line.p0.x + t1 * line.p1.x,
)
}
/// Checks the relative order of two line segments.
///
/// The two line segments must both be increasing in `y`, and their `y` ranges
/// must overlap.
pub fn intersect_lines(l0: Line, l1: Line, tolerance: f64) -> CurveOrder {
let y0 = l0.p0.y.max(l1.p0.y);
let y1 = l0.p1.y.min(l1.p1.y);
let mut ret = CurveOrder::new(y0);
let status = MetricsStatus::default();
debug_assert!(y0 <= y1);
let (l0x0, l0x1) = line_at_ys(l0, y0, y1);
let (l1x0, l1x1) = line_at_ys(l1, y0, y1);
let dx0 = l1x0 - l0x0;
let dx1 = l1x1 - l0x1;
if dx0.abs() <= tolerance && dx1.abs() <= tolerance {
ret.push(y1, Order::Ish, status);
return ret;
} else if (dx0 - dx1).abs() <= tolerance / 2.0 {
// The lines are almost parallel: we'll mark them as always having the
// same order.
let order = if dx0 <= -tolerance || dx1 <= -tolerance {
Order::Right
} else if dx0 >= tolerance || dx1 >= tolerance {
Order::Left
} else {
Order::Ish
};
ret.push(y1, order, status);
return ret;
}
// The t at which l0 crosses l1 - tolerance.
let t0 = (dx0 + tolerance) / (dx0 - dx1);
let t1 = (dx0 - tolerance) / (dx0 - dx1);
let z0 = y0 + (y1 - y0) * t0;
let z1 = y0 + (y1 - y0) * t1;
// For a second, consider the whole lines and not the segments. If dx0 < dx1,
// l0 starts off Left, then they become Ish, then l0 is Right. If dx1 > dx0,
// they go Right, Ish, Left.
//
// Once we restrict to the line segments instead of the lines, the basic
// orderings are the same but they might get truncated.
if dx0 < dx1 {
debug_assert!(z0 <= z1);
if z0 > y0 {
ret.push(z0.min(y1), Order::Right, status);
if z0 >= y1 {
return ret;
}
}
if z1 > y0 && z1 > z0 {
ret.push(z1.min(y1), Order::Ish, status);
if z1 >= y1 {
return ret;
}
}
ret.push(y1, Order::Left, status);
} else {
debug_assert!(z0 >= z1);
if z1 > y0 {
ret.push(z1.min(y1), Order::Left, status);
if z1 >= y1 {
return ret;
}
}
if z0 > y0 && z0 > z1 {
ret.push(z0.min(y1), Order::Ish, status);
if z0 >= y1 {
return ret;
}
}
ret.push(y1, Order::Right, status);
}
ret
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn simple_intersection() {
let l0 = Line::new((0.0, 0.0), (0.0, 1.0));
let l1 = Line::new((1.0, 0.0), (-1.0, 1.0));
let order = intersect_lines(l0, l1, 1e-6);
assert_eq!(order.cmps.len(), 3);
assert_eq!(order.cmps[0].order, Order::Left);
assert_eq!(order.cmps[1].order, Order::Ish);
assert_eq!(order.cmps[2].order, Order::Right);
assert!((order.cmps[0].end - 0.5).abs() < 1e-5);
let order = intersect_lines(l1, l0, 1e-6);
assert_eq!(order.cmps.len(), 3);
assert_eq!(order.cmps[0].order, Order::Right);
assert_eq!(order.cmps[1].order, Order::Ish);
assert_eq!(order.cmps[2].order, Order::Left);
assert!((order.cmps[0].end - 0.5).abs() < 1e-5);
}
#[test]
fn unchanging() {
let l0 = Line::new((-1.0, 0.0), (-1.0, 1.0));
let l1 = Line::new((1.0, 0.0), (1.0, 1.0));
let order = intersect_lines(l0, l1, 1e-6);
assert_eq!(order.cmps.len(), 1);
assert_eq!(order.cmps[0].order, Order::Left);
let l0 = Line::new((-2.0, 0.0), (0.0, 1.0));
let l1 = Line::new((1.0, 0.0), (1.0, 1.0));
let order = intersect_lines(l0, l1, 1e-6);
assert_eq!(order.cmps.len(), 1);
assert_eq!(order.cmps[0].order, Order::Left);
}
#[test]
fn close_near_endpoint() {
let l0 = Line::new((0.0, 0.0), (0.0, 1.0));
let l1 = Line::new((1e-7, 0.0), (-1.0, 1.0));
let order = intersect_lines(l0, l1, 1e-6);
assert_eq!(order.cmps.len(), 2);
assert_eq!(order.cmps[0].order, Order::Ish);
assert_eq!(order.cmps[1].order, Order::Right);
assert!(order.cmps[0].end.abs() < 1e-5);
let l0 = Line::new((0.0, 0.0), (0.0, 1.0));
let l1 = Line::new((-1.0, 0.0), (1e-7, 1.0));
let order = intersect_lines(l0, l1, 1e-6);
assert_eq!(order.cmps.len(), 2);
assert_eq!(order.cmps[0].order, Order::Right);
assert_eq!(order.cmps[1].order, Order::Ish);
assert!((order.cmps[0].end - 1.0).abs() < 1e-5);
}
}
File diff suppressed because it is too large. Load diff
+57
View File
@@ -0,0 +1,57 @@
#![allow(missing_docs)]
#![allow(dead_code)]
use arrayvec::ArrayVec;
use super::Quadratic;
pub struct SplitQuadPart {
quad: Quadratic,
until: f64,
}
pub struct SplitQuad {
quads: ArrayVec<SplitQuadPart, 3>,
}
impl SplitQuad {
pub fn single(q: Quadratic) -> Self {
Self {
quads: [SplitQuadPart {
quad: q,
until: f64::INFINITY,
}]
.into_iter()
.collect(),
}
}
}
pub struct QuadBound {
upper: SplitQuad,
lower: SplitQuad,
}
impl QuadBound {
pub fn new(mut q: Quadratic, upper: f64, lower: f64, shift: f64) -> Self {
debug_assert!(lower <= 0.0 && 0.0 <= upper);
debug_assert!(shift >= 0.0);
let center = -q.c1 / (2.0 * q.c0);
let extremum = q.c0 + q.c1 * center;
if center.is_infinite() || extremum.is_infinite() {
// Our quad is close enough to linear.
q.c2 = 0.0;
let upper = q + upper;
let lower = q + lower;
let signed_shift = if q.c1 > 0.0 { shift } else { -shift };
Self {
upper: SplitQuad::single(upper.shift(-signed_shift)),
lower: SplitQuad::single(lower.shift(signed_shift)),
}
} else {
todo!()
}
}
}
+189
View File
@@ -0,0 +1,189 @@
//! Conservative checks for transversal intersections.
//!
//! We go to some effort to output curves with the correct horizontal
//! order. When curves are close to one another, our general solution
//! involves approximation with axis-aligned quadratics. This works,
//! but can lead to lots of subdivision. This module is about
//! improving a common case: when two curves intersect each other
//! transversally, they are close to one another near the intersection
//! point, but we can still tell that they are ordered. Hence, we
//! can avoid subdividing.
use kurbo::{CubicBez, ParamCurve, ParamCurveDeriv, QuadBez};
use crate::curve::solve_t_for_y;
// Finds the minimum of `a t^2 + 2 b t (1 - t) + c (1 - t)^2` in the given range of t.
fn quad_max_between(a: f64, b: f64, c: f64, range: std::ops::RangeInclusive<f64>) -> f64 {
// We don't check for inf or nan here, but our subsequent comparisons will filter
// those out. (The order of the comparisons is important, because nan always compares
// false.)
let critical_t = (c - b) / (a - b + (c - b));
let t0 = *range.start();
let t1 = *range.end();
let eval = |t: f64| a * t * t + 2.0 * b * t * (1.0 - t) + c * (1.0 - t) * (1.0 - t);
let mut max = eval(t0).max(eval(t1));
if critical_t > t0 && critical_t < t1 {
max = max.max(eval(critical_t));
}
max
}
fn quad_min_between(a: f64, b: f64, c: f64, range: std::ops::RangeInclusive<f64>) -> f64 {
-quad_max_between(-a, -b, -c, range)
}
pub fn transversal_after(left: CubicBez, right: CubicBez, y1: f64) -> bool {
debug_assert_eq!(left.start().y, right.start().y);
debug_assert!(left.start().x <= right.start().x);
debug_assert!(left.start().y < y1);
// First, check if they're transversal at the initial point. We exclude
// curves whose tangents are close to degenerate here, just to make things
// nicer numerically.
let left_tangent = left.p1 - left.p0;
let right_tangent = right.p1 - right.p0;
if left_tangent.hypot2() < 1e-20 || right_tangent.hypot2() < 1e-20 {
return false;
}
let cross = left_tangent.cross(right_tangent);
if left_tangent.dot(right_tangent) > 0.0
&& cross * cross < 1e-2 * left_tangent.hypot2() * right_tangent.hypot2()
{
return false;
}
let t1_left = solve_t_for_y(left, y1);
let QuadBez {
p0: l0,
p1: l1,
p2: l2,
} = left.deriv();
let left_max_dx_dt = quad_max_between(l2.x, l1.x, l0.x, 0.0..=t1_left);
let t1_right = solve_t_for_y(right, y1);
let QuadBez {
p0: r0,
p1: r1,
p2: r2,
} = right.deriv();
let right_min_dx_dt = quad_min_between(r2.x, r1.x, r0.x, 0.0..=t1_right);
if left_max_dx_dt < 0.0 && right_min_dx_dt > 0.0 {
return true;
}
let left_dy_dt = if left_max_dx_dt < 0.0 {
quad_max_between(l2.y, l1.y, l0.y, 0.0..=t1_left)
} else {
quad_min_between(l2.y, l1.y, l0.y, 0.0..=t1_left)
};
let right_dy_dt = if right_min_dx_dt < 0.0 {
quad_min_between(r2.y, r1.y, r0.y, 0.0..=t1_right)
} else {
quad_max_between(r2.y, r1.y, r0.y, 0.0..=t1_right)
};
left_max_dx_dt * right_dy_dt < right_min_dx_dt * left_dy_dt
}
pub fn transversal_before(left: CubicBez, right: CubicBez, y0: f64) -> bool {
// Reflect everything in y.
let p = |q: kurbo::Point| kurbo::Point { x: q.x, y: -q.y };
let c = |d: CubicBez| CubicBez {
p0: p(d.p3),
p1: p(d.p2),
p2: p(d.p1),
p3: p(d.p0),
};
transversal_after(c(left), c(right), -y0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_transversal() {
let left = CubicBez {
p0: (0., 0.).into(),
p1: (-1., 1.).into(),
p2: (-1., 1.).into(),
p3: (0., 2.).into(),
};
let right = CubicBez {
p0: (0., 0.).into(),
p1: (1., 1.).into(),
p2: (1., 1.).into(),
p3: (0., 2.).into(),
};
assert!(transversal_after(left, right, 0.5));
// It isn't really necessary for the implementation to pass
// these two tests; they just validate that our particular
// implementation does what we think. Our implementation looks
// only at derivatives, and the derivatives change direction
// at y = 1.0.
assert!(transversal_after(left, right, 0.9));
assert!(!transversal_after(left, right, 1.1));
}
#[test]
fn parallel() {
let left = CubicBez {
p0: (0., 0.).into(),
p1: (-1., 1.).into(),
p2: (-1., 1.).into(),
p3: (0., 2.).into(),
};
let right = CubicBez {
p0: (0., 0.).into(),
p1: (-1., 1.).into(),
p2: (1., 1.).into(),
p3: (0., 2.).into(),
};
assert!(!transversal_after(left, right, 0.01));
}
#[test]
fn parallel_and_horizontal() {
let left = CubicBez {
p0: (0., 0.).into(),
p1: (-1., 0.).into(),
p2: (-1., 1.).into(),
p3: (0., 2.).into(),
};
let right = CubicBez {
p0: (0., 0.).into(),
p1: (-1., 0.).into(),
p2: (1., 1.).into(),
p3: (0., 2.).into(),
};
assert!(!transversal_after(left, right, 0.01));
}
#[test]
fn antiparallel_and_horizontal() {
let left = CubicBez {
p0: (0., 0.).into(),
p1: (-1., 0.).into(),
p2: (-1., 1.).into(),
p3: (0., 2.).into(),
};
let right = CubicBez {
p0: (0., 0.).into(),
p1: (1., 0.).into(),
p2: (1., 1.).into(),
p3: (0., 2.).into(),
};
assert!(transversal_after(left, right, 0.01));
}
}
+147
View File
@@ -0,0 +1,147 @@
//! Utilities for generating examples, benchmarks, and test cases.
use std::f64::consts::PI;
use kurbo::{Affine, BezPath};
use crate::Point;
type Contours = Vec<Vec<Point>>;
/// Generate a bunch of squares, arranged in a grid.
///
/// The top-left of the first square is at (x0, y0). Each square has size `size
/// x size`, and the distance between squares (both horizontally and vertically)
/// is `offset`.
///
/// If `slant` is non-zero, generates parallelograms instead of squares: the
/// right-hand side of each square gets translated down by `slant`.
fn squares((x0, y0): (f64, f64), size: f64, offset: f64, slant: f64, count: usize) -> Contours {
let mut ret = Vec::new();
for i in 0..count {
let x = x0 + i as f64 * offset;
for j in 0..count {
let y = y0 + j as f64 * offset;
ret.push(vec![
Point::new(x, y),
Point::new(x, y + size),
Point::new(x + size, y + size + slant),
Point::new(x + size, y + slant),
]);
}
}
ret
}
/// Generate an `n` by `n` checkerboard-like pattern with overlapping squares.
/// For `n = 3`, it looks like:
///
/// ```text
/// ┌────┐ ┌────┐ ┌────┐
/// │ │ │ │ │ │
/// │ ┌─┼─┼─┐┌─┼─┼─┐ │
/// └──┼─┘ └─┼┼─┘ └─┼──┘
/// ┌──┼─┐ ┌─┼┼─┐ ┌─┼──┐
/// │ └─┼─┼─┘└─┼─┼─┘ │
/// │ ┌─┼─┼─┐┌─┼─┼─┐ │
/// └──┼─┘ └─┼┼─┘ └─┼──┘
/// ┌──┼─┐ ┌─┼┼─┐ ┌─┼──┐
/// │ └─┼─┼─┘└─┼─┼─┘ │
/// │ │ │ │ │ │
/// └────┘ └────┘ └────┘
/// ```
///
/// We return the pattern in two parts: the outer collection of `n x n`
/// non-overlapping squares, and the inner collection of `(n - 1) x (n - 1)`
/// non-overlapping squares.
pub fn checkerboard(n: usize) -> (Contours, Contours) {
(
squares((0.0, 0.0), 30.0, 40.0, 0.0, n),
squares((20.0, 20.0), 30.0, 40.0, 0.0, n - 1),
)
}
/// Like `checkerboard`, but with no exactly-horizontal lines.
///
/// Horizontal lines have special handling in the sweep-line algorithm, so
/// their presence or absence can affect performance.
pub fn slanted_checkerboard(n: usize) -> (Contours, Contours) {
(
squares((0.0, 0.0), 30.0, 40.0, 1.0, n),
squares((20.0, 20.0), 30.0, 40.0, 1.0, n - 1),
)
}
/// The "evens" are a bunch of long, skinny parallelograms going from top-left
/// to bottom-right. The "odds" go from top-right to bottom-left.
pub fn slanties(n: usize) -> (Contours, Contours) {
let h = 20.0 * n as f64;
let mut even = Vec::new();
let mut odd = Vec::new();
for i in 0..n {
let x_off = 20.0 * i as f64;
even.push(vec![
Point::new(x_off, 0.0),
Point::new(x_off + h, h),
Point::new(x_off + h + 10.0, h),
Point::new(x_off + 10.0, 0.0),
]);
odd.push(vec![
Point::new(x_off + h, 0.0),
Point::new(x_off, h),
Point::new(x_off + 10.0, h),
Point::new(x_off + h + 10.0, 0.0),
]);
}
(even, odd)
}
/// A bunch of things that all intersect at the origin, some tangentially.
pub fn star(n: usize) -> (BezPath, BezPath) {
fn mul(p: kurbo::Point, f: f64) -> kurbo::Point {
(p.x * f, p.y * f).into()
}
let angle_increment = PI / (n as f64 * 2.0);
let mut angle = 0.0;
let mut straight = BezPath::new();
let mut bent = BezPath::new();
let p = kurbo::Point::new(1.0, 0.0);
for i in 0..n {
let p0 = Affine::rotate(angle) * p;
let p1 = Affine::rotate(angle + angle_increment) * p;
let p2 = Affine::rotate(angle + angle_increment + PI) * p;
let p3 = Affine::rotate(angle + PI) * p;
if i.is_multiple_of(2) {
straight.move_to(p0);
straight.line_to(p1);
straight.line_to(p2);
straight.line_to(p3);
straight.close_path();
} else {
let zero_ish = mul(p0, 1e-12);
let neg_zero_ish = mul(p0, -1e-12);
bent.move_to(p0);
bent.line_to(p1);
bent.curve_to(mul(p1, 2. / 3.), mul(p0.midpoint(p1), 1. / 3.), zero_ish);
bent.curve_to(mul(p2.midpoint(p3), 1. / 3.), mul(p2, 2. / 3.), p2);
bent.line_to(p3);
bent.curve_to(
mul(p3, 2. / 3.),
mul(p2.midpoint(p3), 1. / 3.),
neg_zero_ish,
);
bent.curve_to(mul(p0.midpoint(p1), 1. / 3.), mul(p0, 2. / 3.), p0);
}
angle += 2.0 * angle_increment;
}
(straight, bent)
}
+542
View File
@@ -0,0 +1,542 @@
//! Geometric primitives, like points and lines.
use arrayvec::ArrayVec;
use kurbo::{CubicBez, ParamCurve, ParamCurveExtrema, QuadBez};
use polycool::Quadratic;
use crate::curve::{quadratic_signs, solve_t_for_y, solve_x_for_y};
use crate::num::CheapOrderedFloat;
/// A two-dimensional point.
///
/// Points are sorted by `y` and then by `x`, for the convenience of our sweep-line
/// algorithm (which moves in increasing `y`).
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, PartialEq)]
pub struct Point {
/// Vertical coordinate.
///
/// Although it isn't important for functionality, the documentation and method naming
/// assumes that larger values are down.
pub y: f64,
/// Horizontal component.
///
/// Although it isn't important for functionality, the documentation and method naming
/// assumes that larger values are to the right.
pub x: f64,
}
impl Ord for Point {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
(
CheapOrderedFloat::from(self.y),
CheapOrderedFloat::from(self.x),
)
.cmp(&(
CheapOrderedFloat::from(other.y),
CheapOrderedFloat::from(other.x),
))
}
}
impl PartialOrd for Point {
#[inline(always)]
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Eq for Point {}
impl std::fmt::Debug for Point {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "({:?}, {:?})", self.x, self.y)
}
}
impl Point {
/// Create a new point.
///
/// Note that the `x` coordinate comes first. This might be a tiny bit
/// confusing because we're sorting by `y` coordinate first, but `(x, y)` is
/// the only sane order (prove me wrong).
pub fn new(x: f64, y: f64) -> Self {
debug_assert!(x.is_finite());
debug_assert!(y.is_finite());
Point { x, y }
}
/// Compute an affine combination between `self` and `other`; that is, `(1 - t) * self + t * other`.
///
/// Panics if a NaN is encountered, which might happen if some input is infinite.
pub fn affine(&self, other: &Self, t: f64) -> Self {
Point {
x: (1.0 - t) * self.x + t * other.x,
y: (1.0 - t) * self.y + t * other.y,
}
}
/// Compatibility with `kurbo` points.
pub fn to_kurbo(self) -> kurbo::Point {
kurbo::Point::new(self.x, self.y)
}
/// Compatibility with `kurbo` points.
pub fn from_kurbo(c: kurbo::Point) -> Self {
Self::new(c.x, c.y)
}
}
impl From<(f64, f64)> for Point {
fn from((x, y): (f64, f64)) -> Self {
Self { x, y }
}
}
impl From<Point> for kurbo::Point {
fn from(p: Point) -> Self {
kurbo::Point::new(p.x, p.y)
}
}
impl From<kurbo::Point> for Point {
fn from(p: kurbo::Point) -> Self {
Point::new(p.x, p.y)
}
}
/// A contour segment.
#[derive(Clone, PartialEq, Eq)]
pub struct Segment {
inner: SegmentInner,
}
#[derive(Clone, PartialEq, Eq)]
enum SegmentInner {
Line {
p0: Point,
p1: Point,
},
Quadratic {
p0: Point,
p1: Point,
p2: Point,
},
Cubic {
p0: Point,
p1: Point,
p2: Point,
p3: Point,
},
}
impl std::fmt::Debug for Segment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.inner {
SegmentInner::Line { p0, p1 } => write!(f, "{p0:?} -- {p1:?}"),
SegmentInner::Quadratic { p0, p1, p2 } => write!(f, "{p0:?} -- {p1:?} -- {p2:?}"),
SegmentInner::Cubic { p0, p1, p2, p3 } => {
write!(f, "{p0:?} -- {p1:?} -- {p2:?} -- {p3:?}")
}
}
}
}
/// A y-monotonic piece of a cubic Bezier.
#[derive(Debug)]
pub(crate) struct MonotonicPiece {
pub start_t: f64,
pub end_t: f64,
pub piece: Segment,
pub positively_oriented: bool,
}
pub(crate) fn monotonic_pieces(s: kurbo::PathSeg) -> ArrayVec<MonotonicPiece, 3> {
match s {
kurbo::PathSeg::Line(ell) => {
let mut ret = ArrayVec::new();
let p0: Point = ell.p0.into();
let p1: Point = ell.p1.into();
let (p0, p1, positively_oriented) = if p0 <= p1 {
(p0, p1, true)
} else {
(p1, p0, false)
};
if p0 != p1 {
ret.push(MonotonicPiece {
start_t: 0.0,
end_t: 1.0,
piece: Segment::straight(p0, p1),
positively_oriented,
});
}
ret
}
kurbo::PathSeg::Quad(q) => monotonic_quadratic_pieces(q),
kurbo::PathSeg::Cubic(c) => monotonic_cubic_pieces(c),
}
}
/// This could return an `ArrayVec<_, 2>`, but by returning `ArrayVec<_, 3>` we
/// make it the same return type as `monotonic_cubic_pieces`.
fn monotonic_quadratic_pieces(q: QuadBez) -> ArrayVec<MonotonicPiece, 3> {
fn to_seg(q: QuadBez) -> Option<(Segment, bool)> {
let (p0, mut p1, p2, orient) = if (q.p0.y, q.p0.x) <= (q.p2.y, q.p2.x) {
(q.p0, q.p1, q.p2, true)
} else {
(q.p2, q.p1, q.p0, false)
};
if p0 != p2 {
// Our derivative-solving should have made this cubic monotonic, but
// there might be some numerical issues with the starting and ending
// tangents. Fix them up.
p1.y = p0.y.max(p1.y).min(p2.y);
let seg = Segment::monotonic_quadratic(p0.into(), p1.into(), p2.into());
Some((seg, orient))
} else {
None
}
}
// The derivative is 2(1 - t) (p1 - p0) + 2t (p2 - p1), and we want to solve
// for when the y coordinate of this derivative is zero.
let t = (q.p0.y - q.p1.y) / (q.p2.y - 2.0 * q.p1.y + q.p0.y);
if 0.0 < t && t < 1.0 {
let q0 = q.subsegment(0.0..t);
let q1 = q.subsegment(t..1.0);
if let (Some((piece0, orient0)), Some((piece1, orient1))) = (to_seg(q0), to_seg(q1)) {
let mut ret = ArrayVec::new();
ret.push(MonotonicPiece {
start_t: 0.0,
end_t: t,
piece: piece0,
positively_oriented: orient0,
});
ret.push(MonotonicPiece {
start_t: t,
end_t: 1.0,
piece: piece1,
positively_oriented: orient1,
});
return ret;
}
}
let mut ret = ArrayVec::new();
if let Some((piece, positively_oriented)) = to_seg(q) {
ret.push(MonotonicPiece {
start_t: 0.0,
end_t: 1.0,
piece,
positively_oriented,
});
}
ret
}
pub(crate) fn monotonic_cubic_pieces(cub: CubicBez) -> ArrayVec<MonotonicPiece, 3> {
fn to_seg(c: CubicBez) -> Option<(Segment, bool)> {
let (p0, mut p1, mut p2, p3, orient) = if (c.p0.y, c.p0.x) <= (c.p3.y, c.p3.x) {
(c.p0, c.p1, c.p2, c.p3, true)
} else {
(c.p3, c.p2, c.p1, c.p0, false)
};
if p0 != p3 {
// Our derivative-solving should have made this cubic monotonic, but
// there might be some numerical issues with the starting and ending
// tangents. Fix them up.
p1.y = p0.y.max(p1.y);
p2.y = p3.y.min(p2.y);
let seg = Segment::monotonic_cubic(p0.into(), p1.into(), p2.into(), p3.into());
Some((seg, orient))
} else {
None
}
}
let mut ret = ArrayVec::new();
let q0 = cub.p1.y - cub.p0.y;
let q1 = cub.p2.y - cub.p1.y;
let q2 = cub.p3.y - cub.p2.y;
// Convert to the representation a t^2 + b t + c.
let c = q0;
let b = (q1 - q0) * 2.0;
let a = q0 - q1 * 2.0 + q2;
let q = Quadratic::new([c, b, a]);
let (_, r0, r1) = quadratic_signs(q);
let mut roots: ArrayVec<f64, 2> = ArrayVec::new();
if r0 > 0.0 && r0 < 1.0 {
roots.push(r0);
}
if r1 > r0 && r1 > 0.0 && r1 < 1.0 {
roots.push(r1);
}
let mut last_r = 0.0;
// We could try to remove very short segments, which could easily happen
// if the input curve has an initial or final tangent that's very close
// to horizontal. For now, we only remove segments that are exactly
// zero-length.
for r in roots {
let piece_before = cub.subsegment(last_r..r);
if let Some((piece, positively_oriented)) = to_seg(piece_before) {
ret.push(MonotonicPiece {
piece,
positively_oriented,
start_t: last_r,
end_t: r,
});
last_r = r;
}
}
let piece_before = cub.subsegment(last_r..1.0);
if let Some((piece, positively_oriented)) = to_seg(piece_before) {
ret.push(MonotonicPiece {
piece,
positively_oriented,
start_t: last_r,
end_t: 1.0,
});
} else if let Some(last_piece) = ret.last_mut() {
last_piece.end_t = 1.0;
}
ret
}
impl Segment {
/// Create a new cubic segment that must be increasing in `y`.
pub fn monotonic_cubic(p0: Point, p1: Point, p2: Point, p3: Point) -> Self {
if p3.y == p0.y {
// Ensure that horizontal segments are just represented by straight lines.
// TODO: maybe we should do something about degenerate S-shaped curves?
Self::straight(p0, p3)
} else {
Self {
inner: SegmentInner::Cubic { p0, p1, p2, p3 },
}
}
}
/// Create a new quadratic segment that must be increasing in `y`.
pub fn monotonic_quadratic(p0: Point, p1: Point, p2: Point) -> Self {
if p2.y == p0.y {
// Ensure that horizontal segments are just represented by straight lines.
// TODO: maybe we should do something about degenerate C-shaped curves?
Self::straight(p0, p2)
} else {
Self {
inner: SegmentInner::Quadratic { p0, p1, p2 },
}
}
}
/// Create a new segment that's just a straight line.
///
/// `start` must be less than `end`.
pub fn straight(start: Point, end: Point) -> Self {
debug_assert!(start <= end);
Self {
inner: SegmentInner::Line { p0: start, p1: end },
}
}
/// The starting point (smallest in the sweep-line order) of this segment.
pub fn start(&self) -> Point {
match self.inner {
SegmentInner::Line { p0, .. }
| SegmentInner::Quadratic { p0, .. }
| SegmentInner::Cubic { p0, .. } => p0,
}
}
/// The ending point (largest in the sweep-line order) of this segment.
pub fn end(&self) -> Point {
match self.inner {
SegmentInner::Line { p1, .. } => p1,
SegmentInner::Quadratic { p2, .. } => p2,
SegmentInner::Cubic { p3, .. } => p3,
}
}
/// Compatibility with `kurbo`'s cubics.
pub fn to_kurbo_cubic(&self) -> kurbo::CubicBez {
match self.inner {
SegmentInner::Line { p0: q0, p1: q1 } => {
let p0 = q0.to_kurbo();
let p3 = q1.to_kurbo();
let p1 = p0 + (1. / 3.) * (p3 - p0);
let p2 = p0 + (2. / 3.) * (p3 - p0);
kurbo::CubicBez { p0, p1, p2, p3 }
}
SegmentInner::Quadratic {
p0: q0,
p1: q1,
p2: q2,
} => {
let p0 = q0.to_kurbo();
let p3 = q2.to_kurbo();
let v1 = q1.to_kurbo().to_vec2();
let p1 = ((p0 + 2.0 * v1).to_vec2() / 3.0).to_point();
let p2 = ((p3 + 2.0 * v1).to_vec2() / 3.0).to_point();
kurbo::CubicBez { p0, p1, p2, p3 }
}
SegmentInner::Cubic { p0, p1, p2, p3 } => kurbo::CubicBez {
p0: p0.to_kurbo(),
p1: p1.to_kurbo(),
p2: p2.to_kurbo(),
p3: p3.to_kurbo(),
},
}
}
/// Compatibility with kurbo.
pub fn to_kurbo_seg(&self) -> kurbo::PathSeg {
match self.inner {
SegmentInner::Line { p0, p1 } => kurbo::Line {
p0: p0.to_kurbo(),
p1: p1.to_kurbo(),
}
.into(),
SegmentInner::Quadratic { p0, p1, p2 } => kurbo::QuadBez {
p0: p0.to_kurbo(),
p1: p1.to_kurbo(),
p2: p2.to_kurbo(),
}
.into(),
SegmentInner::Cubic { p0, p1, p2, p3 } => kurbo::CubicBez {
p0: p0.to_kurbo(),
p1: p1.to_kurbo(),
p2: p2.to_kurbo(),
p3: p3.to_kurbo(),
}
.into(),
}
}
/// TODO: maybe From instead?
pub fn from_kurbo_cubic(c: kurbo::CubicBez) -> Self {
Self::monotonic_cubic(
Point::from_kurbo(c.p0),
Point::from_kurbo(c.p1),
Point::from_kurbo(c.p2),
Point::from_kurbo(c.p3),
)
}
/// Is this segment just a straight line?
pub fn is_line(&self) -> bool {
matches!(self.inner, SegmentInner::Line { .. })
}
/// If this segment is a line, returns it.
pub fn as_kurbo_line(&self) -> Option<kurbo::Line> {
match self.inner {
SegmentInner::Line { p0, p1 } => Some(kurbo::Line::new(p0.to_kurbo(), p1.to_kurbo())),
_ => None,
}
}
/// A crude lower bound on our minimum horizontal position.
pub fn min_x(&self) -> f64 {
match self.inner {
SegmentInner::Line { p0, p1 } => p0.x.min(p1.x),
SegmentInner::Quadratic { p0, p1, p2 } => p0.x.min(p1.x).min(p2.x),
SegmentInner::Cubic { p0, p1, p2, p3 } => p0.x.min(p1.x).min(p2.x).min(p3.x),
}
}
/// A crude upper bound on our maximum horizontal position.
pub fn max_x(&self) -> f64 {
match self.inner {
SegmentInner::Line { p0, p1 } => p0.x.max(p1.x),
SegmentInner::Quadratic { p0, p1, p2 } => p0.x.max(p1.x).max(p2.x),
SegmentInner::Cubic { p0, p1, p2, p3 } => p0.x.max(p1.x).max(p2.x).max(p3.x),
}
}
/// Our `x` coordinate at the given `y` coordinate.
///
/// Horizontal segments will return their largest `x` coordinate.
///
/// # Panics
///
/// Panics if `y` is outside the `y` range of this segment.
pub fn at_y(&self, y: f64) -> f64 {
debug_assert!(
(self.start().y..=self.end().y).contains(&y),
"segment {self:?}, y={y:?}"
);
match self.inner {
SegmentInner::Line { p0, p1 } => {
if self.is_horizontal() {
p1.x
} else {
let t = (y - p0.y) / (p1.y - p0.y);
p0.x + t * (p1.x - p0.x)
}
}
// We could have a more optimized implementation for qudratics.
SegmentInner::Quadratic { .. } | SegmentInner::Cubic { .. } => {
solve_x_for_y(self.to_kurbo_cubic(), y)
}
}
}
fn local_bbox(&self, y: f64, eps: f64) -> kurbo::Rect {
let start_y = (y - eps).max(self.start().y);
let end_y = (y + eps).min(self.end().y);
match self.inner {
SegmentInner::Line { .. } => {
let start_x = self.at_y(start_y);
let end_x = self.at_y(end_y);
kurbo::Rect::from_points((start_x, start_y), (end_x, end_y))
}
SegmentInner::Quadratic { .. } | SegmentInner::Cubic { .. } => {
let c = self.to_kurbo_cubic();
let t_min = solve_t_for_y(c, start_y);
let t_max = solve_t_for_y(c, end_y);
c.subsegment(t_min..t_max).bounding_box()
}
}
}
/// Returns a lower bound on the `x` coordinate near `y`.
///
/// More precisely, take a Minkowski sum between this curve
/// and a small square. This returns a lower bound on the `x`
/// coordinate of the resulting set at height `y`.
pub fn lower(&self, y: f64, eps: f64) -> f64 {
self.local_bbox(y, eps).min_x() - eps
}
/// Returns an upper bound on the `x` coordinate near `y`.
///
/// More precisely, take a Minkowski sum between this curve
/// and a small square. This returns a upper bound on the `x`
/// coordinate of the resulting set at height `y`.
pub fn upper(&self, y: f64, eps: f64) -> f64 {
self.local_bbox(y, eps).max_x() + eps
}
/// Returns true if this segment is exactly horizontal.
pub fn is_horizontal(&self) -> bool {
match self.inner {
SegmentInner::Line { p0, p1 } => p0.y == p1.y,
SegmentInner::Cubic { .. } | SegmentInner::Quadratic { .. } => {
debug_assert_ne!(
self.start().y,
self.end().y,
"horizontal segments should be represented by lines"
);
false
}
}
}
}
+336
View File
@@ -0,0 +1,336 @@
#![deny(missing_docs)]
#![doc = include_str!("../README.md")]
#[macro_use]
mod typed_vec;
#[cfg(any(test, feature = "arbitrary"))]
pub mod arbitrary;
pub mod curve;
mod geom;
mod num;
pub mod order;
mod position;
mod segments;
pub mod sweep;
pub mod topology;
#[cfg(feature = "generators")]
pub mod generators;
pub use geom::{Point, Segment};
use kurbo::Shape;
pub use segments::{SegIdx, Segments};
// pub so that we can use it in fuzz tests, but it's really private
#[doc(hidden)]
pub mod treevec;
use topology::{BinaryWindingNumber, Topology};
use crate::segments::NonClosedPath;
#[cfg(test)]
pub mod perturbation;
/// A fill rule tells us how to decide whether a point is "inside" a polyline.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum FillRule {
/// The point is "inside" if its winding number is odd.
EvenOdd,
/// The point is "inside" if its winding number is non-zero.
NonZero,
}
impl FillRule {
/// According to this fill rule, is the winding number inside the shape?
pub fn is_inside(self, winding: i32) -> bool {
match self {
FillRule::EvenOdd => winding % 2 != 0,
FillRule::NonZero => winding != 0,
}
}
}
/// Binary operations between sets.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum BinaryOp {
/// A point is in the union of two sets if it is in either one.
Union,
/// A point is in the intersection of two sets if it is in both.
Intersection,
/// A point is in the difference of two sets if it is in the first but not the second.
Difference,
/// A point is in the exclusive-or of two sets if it is in one or the other, but not both.
Xor,
}
#[derive(Clone, Copy, Debug, PartialEq)]
/// The input points were faulty.
pub enum Error {
/// At least one of the inputs was infinite.
Infinity,
/// At least one of the inputs was not a number.
NaN,
/// One of the inputs had a non-closed path.
NonClosedPath(NonClosedPath),
}
impl From<NonClosedPath> for Error {
fn from(ncp: NonClosedPath) -> Self {
Error::NonClosedPath(ncp)
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Infinity => write!(f, "one of the inputs was infinite"),
Error::NaN => write!(f, "one of the inputs had a NaN"),
Error::NonClosedPath(_) => write!(f, "one of the inputs had a non-closed path"),
}
}
}
impl std::error::Error for Error {}
/// Computes a boolean operation between two sets, each of which is described as
/// a collection of closed curves.
///
/// This function calls [`binary_op_with_eps`] with an automatically-chosen
/// accuracy parameter. The automatically-chosen accuracy is fairly
/// conservative, so you may get better performance by choosing your own.
pub fn binary_op(
set_a: &kurbo::BezPath,
set_b: &kurbo::BezPath,
fill_rule: FillRule,
op: BinaryOp,
) -> Result<topology::Contours, Error> {
// Find the extremal values, to figure out how much precision we can support.
let bbox = set_a.bounding_box().union(set_b.bounding_box());
let min = bbox.min_x().min(bbox.min_y());
let max = bbox.max_x().max(bbox.max_y());
if min.is_infinite() || max.is_infinite() {
return Err(Error::Infinity);
}
// If there was any NaN in the input, it should have propagated to the min and max.
if min.is_nan() || max.is_nan() {
return Err(Error::NaN);
}
// TODO: we did some analysis for error bounds in the case of polylines.
// Think more about what makes sense for curves.
let m_2 = min.abs().max(max.abs());
let eps = m_2 * (f64::EPSILON * 64.0);
let eps = eps.max(1e-6);
debug_assert!(eps.is_finite());
binary_op_with_eps(set_a, set_b, fill_rule, op, eps)
}
/// Computes a boolean operation between two sets, each of which is described as
/// a collection of closed curves.
///
/// The output paths will not exactly agree with the input paths in general.
/// The accuracy parameter `eps` bounds how much they can differ.
pub fn binary_op_with_eps(
set_a: &kurbo::BezPath,
set_b: &kurbo::BezPath,
fill_rule: FillRule,
op: BinaryOp,
eps: f64,
) -> Result<topology::Contours, Error> {
let top = Topology::from_paths_binary(set_a, set_b, eps)?;
#[cfg(feature = "debug-svg")]
{
svg::save(
"out.svg",
&top.dump_svg(|tag| {
if tag == topology::OneOfTwo::A {
"red".to_owned()
} else {
"blue".to_owned()
}
}),
)
.unwrap();
}
let inside = |windings: &BinaryWindingNumber| {
let inside_one = |winding| match fill_rule {
FillRule::EvenOdd => winding % 2 != 0,
FillRule::NonZero => winding != 0,
};
match op {
BinaryOp::Union => inside_one(windings.shape_a) || inside_one(windings.shape_b),
BinaryOp::Intersection => inside_one(windings.shape_a) && inside_one(windings.shape_b),
BinaryOp::Xor => inside_one(windings.shape_a) != inside_one(windings.shape_b),
BinaryOp::Difference => inside_one(windings.shape_a) && !inside_one(windings.shape_b),
}
};
Ok(top.contours(inside))
}
#[cfg(test)]
mod tests {
use insta::assert_ron_snapshot;
use kurbo::BezPath;
use super::*;
#[test]
fn two_squares() {
fn to_bez(mut points: impl Iterator<Item = (f64, f64)>) -> BezPath {
let p = points.next().unwrap();
let mut ret = BezPath::default();
ret.move_to(p);
for q in points {
ret.line_to(q);
}
ret.line_to(p);
ret
}
let a = vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
let b = vec![(-0.5, -0.5), (0.5, -0.5), (0.5, 0.5), (-0.5, 0.5)];
let output = binary_op(
&to_bez(a.into_iter()),
&to_bez(b.into_iter()),
FillRule::EvenOdd,
BinaryOp::Intersection,
)
.unwrap();
insta::assert_ron_snapshot!(output);
}
// This example has a union of two not-quite-axis-aligned crosses. It
// suffers from some extra quadratic segments near the intersection points,
// but it used to be worse.
#[test]
fn path_blowup() {
let path1 = "M-90.03662872314453,-212 L-90.03662872314453,212 L90.03565216064453,212 L90.03565216064453,-212 L-90.03662872314453,-212 Z";
let path2 = "M211.99964904785156,-90.03582000732422 L-212.00035095214844,-90.03646087646484 L-212.00062561035156,90.03582000732422 L211.99937438964844,90.03646087646484 L211.99964904785156,-90.03582000732422 Z";
if let Ok(output) = binary_op(
&BezPath::from_svg(path1).unwrap(),
&BezPath::from_svg(path2).unwrap(),
FillRule::NonZero,
BinaryOp::Union,
) {
let output = output.contours().next().unwrap();
let path_length = output.path.elements().len();
// This should ideally be more like 12, but there
// are some small horizontal segments in the output.
// It used to be 77, though...
assert!(path_length <= 16);
}
}
#[test]
fn test_empty_horizontals() {
let square = kurbo::Rect::from_origin_size((0.0, 0.0), (10.0, 10.0)).to_path(0.0);
let line_and_back = {
let mut path = kurbo::Line::new((-1.0, 0.0), (1.0, 0.0)).to_path(0.0);
path.close_path();
path
};
let line_and_back_2 = {
let mut path = kurbo::Line::new((2.0, 1.0), (12.0, 1.0)).to_path(0.0);
path.close_path();
path
};
Topology::from_path(&line_and_back, 1e-3).unwrap();
Topology::from_path(&line_and_back_2, 1e-3).unwrap();
Topology::<i32>::from_paths([(&line_and_back, ()), (&line_and_back_2, ())], 1e-3).unwrap();
Topology::<i32>::from_paths(
[(&line_and_back, ()), (&line_and_back_2, ()), (&square, ())],
1e-3,
)
.unwrap();
let output = binary_op(
&square,
&line_and_back_2,
FillRule::EvenOdd,
BinaryOp::Difference,
)
.unwrap();
assert_ron_snapshot!(output);
}
#[test]
fn monotonicity_violation() {
let path = "M-16.252847470703124,8.977497100830078 C-8.126453262439954,8.977497100830078 -3.724613933559983,8.977497100830078 4.401851380682135,8.977497100830078 C4.401851380682135,8.977497100830078 16.252847872314454,8.977497100830075 16.252847872314454,-0.5280563794708262 C16.252847872314454,-8.977497100830078 4.401851380682135,-8.977497100830075 4.401851380682135,-8.977497100830075 L4.401851380682135,-8.977497100830075 C-5.078945812623724,-8.977497100830075 -1.5236468651340243,-8.977497100830075 -11.004562568404802,-8.977497100830075 Z";
let path = BezPath::from_svg(path).unwrap();
Topology::from_path(&path, 0.001).unwrap();
}
#[test]
fn empty_paths() {
let contours = binary_op(
&BezPath::new(),
&BezPath::new(),
FillRule::EvenOdd,
BinaryOp::Xor,
)
.unwrap();
assert!(contours.contours().next().is_none());
}
#[test]
fn repeated_points() {
Topology::from_polylines_binary(
[[
Point::new(0.0, 0.0),
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
Point::new(1.0, 1.0),
]],
[[
Point::new(0.0, 0.0),
Point::new(0.0, 1.0),
Point::new(1.0, 1.0),
]],
1e-6,
);
}
#[test]
fn quadratics_in_quadratics_out() {
let mut quad_path = BezPath::new();
quad_path.move_to((0.0, 0.0));
quad_path.quad_to((1.0, -5.0), (2.0, 0.0));
quad_path.quad_to((1.0, 5.0), (0.0, 0.0));
let mut rectangle = BezPath::new();
rectangle.move_to((-1.0, -1.0));
rectangle.line_to((-1.0, 1.0));
rectangle.line_to((3.0, 1.0));
rectangle.line_to((3.0, -1.0));
rectangle.line_to((-1.0, -1.0));
let mut segs = Segments::default();
segs.add_bez_path(&quad_path).unwrap();
let contours = binary_op(
&quad_path,
&rectangle,
FillRule::EvenOdd,
BinaryOp::Intersection,
)
.unwrap();
assert_eq!(contours.contours().count(), 1);
let contour = contours.contours().next().unwrap();
assert_eq!(
contour
.path
.elements()
.iter()
.filter(|el| matches!(el, &&kurbo::PathEl::QuadTo(..)))
.count(),
4
);
}
}
+132
View File
@@ -0,0 +1,132 @@
//! Some numerical utilities.
use std::hash::Hash;
/// A wrapper for `f64` that implements `Ord`.
///
/// Unlike the more principled wrappers in the `ordered_float` crate, this one
/// just breaks the `Ord` rules when comparing NaNs -- it doesn't order them,
/// nor does it panic or guard against them on construction. This makes things
/// substantially faster: I measured a 20% improvement to some benchmarks by
/// switching from `OrderedFloat` to `CheapOrderedFloat`.
///
/// Ignoring NaNs should be ok: we check input for NaNs and infinities, and
/// then we make some effort not to introduce them.
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(test, serde(transparent))]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CheapOrderedFloat(f64);
impl std::ops::Add<CheapOrderedFloat> for CheapOrderedFloat {
type Output = Self;
fn add(self, rhs: CheapOrderedFloat) -> Self::Output {
CheapOrderedFloat(self.0 + rhs.0)
}
}
impl<'a> std::ops::Add<&'a CheapOrderedFloat> for CheapOrderedFloat {
type Output = Self;
fn add(self, rhs: &'a CheapOrderedFloat) -> Self::Output {
CheapOrderedFloat(self.0 + rhs.0)
}
}
impl std::ops::Sub<CheapOrderedFloat> for CheapOrderedFloat {
type Output = Self;
fn sub(self, rhs: CheapOrderedFloat) -> Self::Output {
CheapOrderedFloat(self.0 - rhs.0)
}
}
impl<'a> std::ops::Sub<&'a CheapOrderedFloat> for CheapOrderedFloat {
type Output = Self;
fn sub(self, rhs: &'a CheapOrderedFloat) -> Self::Output {
CheapOrderedFloat(self.0 - rhs.0)
}
}
impl std::ops::Mul<CheapOrderedFloat> for CheapOrderedFloat {
type Output = Self;
fn mul(self, rhs: CheapOrderedFloat) -> Self::Output {
CheapOrderedFloat(self.0 * rhs.0)
}
}
impl<'a> std::ops::Mul<&'a CheapOrderedFloat> for CheapOrderedFloat {
type Output = Self;
fn mul(self, rhs: &'a CheapOrderedFloat) -> Self::Output {
CheapOrderedFloat(self.0 * rhs.0)
}
}
impl std::ops::Div<CheapOrderedFloat> for CheapOrderedFloat {
type Output = Self;
fn div(self, rhs: CheapOrderedFloat) -> Self::Output {
CheapOrderedFloat(self.0 / rhs.0)
}
}
impl<'a> std::ops::Div<&'a CheapOrderedFloat> for CheapOrderedFloat {
type Output = Self;
fn div(self, rhs: &'a CheapOrderedFloat) -> Self::Output {
CheapOrderedFloat(self.0 / rhs.0)
}
}
impl std::ops::Neg for CheapOrderedFloat {
type Output = Self;
fn neg(self) -> Self::Output {
CheapOrderedFloat(-self.0)
}
}
impl Hash for CheapOrderedFloat {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.to_bits().hash(state)
}
}
impl CheapOrderedFloat {
/// Retrieve the inner `f64`.
pub fn into_inner(self) -> f64 {
self.0
}
}
// Now comes the fishy stuff.
impl Eq for CheapOrderedFloat {}
impl PartialOrd for CheapOrderedFloat {
#[inline(always)]
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for CheapOrderedFloat {
#[inline(always)]
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
if self.0 < other.0 {
std::cmp::Ordering::Less
} else if self.0 > other.0 {
std::cmp::Ordering::Greater
} else {
std::cmp::Ordering::Equal
}
}
}
impl From<f64> for CheapOrderedFloat {
fn from(value: f64) -> Self {
CheapOrderedFloat(value)
}
}
+130
View File
@@ -0,0 +1,130 @@
//! Curve order comparisons, with caching.
use rustc_hash::FxHashMap;
use crate::{
SegIdx, Segments,
curve::{self, CurveOrder},
};
/// A cache for curve comparisons, so that each pair of curves needs to be compared at most once.
#[derive(Clone, Debug)]
pub struct ComparisonCache {
inner: FxHashMap<(SegIdx, SegIdx), (CurveOrder, CurveOrder)>,
accuracy: f64,
tolerance: f64,
}
impl ComparisonCache {
/// Creates a new comparison cache.
///
/// `tolerance` tells us how close two curves can be to be declared "ish", and
/// `accuracy` tells us how closely we need to evaluate the tolerance. For
/// example, if `accuracy` is `tolerance / 2.0` then we'll guarantee (up to
/// some floating-point error) that if the two curves are further than
/// `1.5 * tolerance` apart then we'll give them a strict order, and if they're
/// less than `tolerance / 2.0` apart then we'll give then an "ish" order.
pub fn new(tolerance: f64, accuracy: f64) -> Self {
ComparisonCache {
inner: FxHashMap::default(),
accuracy,
tolerance,
}
}
/// Creates a new comparison cache.
///
/// `tolerance` tells us how close two curves can be to be declared "ish", and
/// `accuracy` tells us how closely we need to evaluate the tolerance. For
/// example, if `accuracy` is `tolerance / 2.0` then we'll guarantee (up to
/// some floating-point error) that if the two curves are further than
/// `1.5 * tolerance` apart then we'll give them a strict order, and if they're
/// less than `tolerance / 2.0` apart then we'll give then an "ish" order.
pub fn new_without_y_slop(tolerance: f64, accuracy: f64) -> Self {
ComparisonCache {
inner: FxHashMap::default(),
accuracy,
tolerance,
}
}
/// Compares two segments, returning their order.
pub fn compare_segments(
&mut self,
segments: &Segments,
i: SegIdx,
j: SegIdx,
) -> &mut CurveOrder {
use std::collections::hash_map::Entry;
let (i, j, flipped) = if i.0 < j.0 {
(i, j, false)
} else {
(j, i, true)
};
match self.inner.entry((i, j)) {
Entry::Occupied(o) => {
if flipped {
&mut o.into_mut().1
} else {
&mut o.into_mut().0
}
}
Entry::Vacant(v) => {
let segi = &segments[i];
let segj = &segments[j];
let forward =
if let (Some(l0), Some(l1)) = (segi.as_kurbo_line(), segj.as_kurbo_line()) {
curve::line::intersect_lines(l0, l1, self.tolerance)
} else {
let c0 = segi.to_kurbo_cubic();
let c0_div = segments.horiz_decomp(i);
let c1 = segj.to_kurbo_cubic();
let c1_div = segments.horiz_decomp(j);
curve::intersect_cubics_with_precomputed_decomp(
c0,
c0_div,
c1,
c1_div,
self.accuracy,
self.tolerance,
)
};
debug_assert_eq!(
forward.iter().last().unwrap().1,
segi.end().y.min(segj.end().y)
);
let reverse = forward.flip();
let v = v.insert((forward, reverse));
if flipped { &mut v.1 } else { &mut v.0 }
}
}
}
}
#[cfg(test)]
mod tests {
use crate::{SegIdx, Segments, curve::Order};
use super::ComparisonCache;
#[test]
fn slop_regression() {
let mut segments = Segments::default();
let eps = 0.1;
segments.add_points([(-0.5, -0.5), (-0.5, 0.5)]);
segments.add_points([(0.0, 0.0), (0.0, 1.0)]);
let mut cmp_cache = ComparisonCache::new(eps, eps / 2.0);
assert_eq!(
cmp_cache
.compare_segments(&segments, SegIdx(0), SegIdx(1))
.iter()
.next()
.unwrap()
.2,
Order::Left
);
}
}
+200
View File
@@ -0,0 +1,200 @@
#![allow(missing_docs)]
//! Functionality for constructing perturbations for testing.
// TODO: add support for curves
// TODO: see how this looks in arbtest
use proptest::{arbitrary::any, prop_oneof, strategy::Strategy};
use crate::geom::Point;
#[derive(Clone, Copy, Debug)]
pub enum F64Perturbation {
/// Perturb by between -128 and 127 ulps.
Ulp(i8),
/// Perturb by a bounded additive amount.
Eps(f64),
}
// The Debug bound is because some of the proptest strategy builders want it. Having it here is
// easier than copying it everywhere.
pub trait FloatPerturbation: std::fmt::Debug {
type Float;
fn apply(&self, f: &Self::Float) -> Self::Float;
}
impl FloatPerturbation for F64Perturbation {
type Float = f64;
fn apply(&self, f: &f64) -> f64 {
match self {
F64Perturbation::Ulp(n) => {
let same_sign = (*n > 0) == (*f > 0.0);
let sign_bit = 1 << 63;
match f.classify() {
std::num::FpCategory::Nan => unreachable!(),
std::num::FpCategory::Infinite => *f,
std::num::FpCategory::Zero => {
let mut bits = n.unsigned_abs().into();
if *n < 0 {
bits |= sign_bit;
}
f64::from_bits(bits)
}
std::num::FpCategory::Subnormal => {
let bits = f.abs().to_bits();
let bits = if same_sign {
bits + u64::from(n.unsigned_abs())
} else {
bits.abs_diff(u64::from(n.unsigned_abs()))
};
f.signum() * f64::from_bits(bits)
}
std::num::FpCategory::Normal => {
let delta = if same_sign {
(*n as i64).abs()
} else {
-(*n as i64).abs()
};
// Taking the absolute value sets the sign bit to zero, so the
// addition should never overflow.
let bits = f.abs().to_bits().checked_add_signed(delta).unwrap();
let x = if bits & sign_bit != 0 {
f64::INFINITY
} else {
f64::from_bits(bits)
};
f.signum() * x
}
}
}
F64Perturbation::Eps(x) => f + x,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct PointPerturbation<P: FloatPerturbation> {
pub x: P,
pub y: P,
}
impl<P: FloatPerturbation<Float = f64>> PointPerturbation<P> {
pub fn apply(&self, p: Point) -> Point {
Point {
x: self.x.apply(&p.x),
y: self.y.apply(&p.y),
}
}
}
#[derive(Clone, Debug)]
pub enum Perturbation<P: FloatPerturbation> {
Base {
idx: usize,
},
Point {
perturbation: PointPerturbation<P>,
idx: usize,
next: Box<Perturbation<P>>,
},
Subdivision {
// Between 0.0 and 1.0
t: P::Float,
idx: usize,
next: Box<Perturbation<P>>,
},
Superimposition {
left: Box<Perturbation<P>>,
right: Box<Perturbation<P>>,
},
}
pub fn f64_perturbation(eps: f64) -> impl Strategy<Value = F64Perturbation> + Clone {
prop_oneof![
any::<i8>().prop_map(F64Perturbation::Ulp),
(-eps..=eps).prop_map(F64Perturbation::Eps)
]
}
pub fn point_perturbation<P: FloatPerturbation>(
fp: impl Strategy<Value = P> + Clone,
) -> impl Strategy<Value = PointPerturbation<P>> {
(fp.clone(), fp).prop_map(|(x, y)| PointPerturbation { x, y })
}
pub fn perturbation(
fp: impl Strategy<Value = F64Perturbation> + Clone + 'static,
) -> impl Strategy<Value = Perturbation<F64Perturbation>> {
let leaf = any::<usize>().prop_map(|idx| Perturbation::Base { idx });
leaf.prop_recursive(3, 16, 8, move |inner| {
prop_oneof![
(
point_perturbation(fp.clone()),
any::<usize>(),
inner.clone()
)
.prop_map(|(perturbation, idx, next)| {
Perturbation::Point {
perturbation,
idx,
next: Box::new(next),
}
}),
(0.0f64..1.0, any::<usize>(), inner.clone()).prop_map(|(t, idx, next)| {
Perturbation::Subdivision {
t,
idx,
next: Box::new(next),
}
}),
(inner.clone(), inner.clone()).prop_map(|(left, right)| {
Perturbation::Superimposition {
left: Box::new(left),
right: Box::new(right),
}
})
]
})
}
fn index<T>(arr: &[T], idx: usize) -> &T {
&arr[idx % arr.len()]
}
fn index_mut<T>(arr: &mut [T], idx: usize) -> &mut T {
&mut arr[idx % arr.len()]
}
pub fn realize_perturbation(
base_cases: &[Vec<Point>],
pert: &Perturbation<F64Perturbation>,
) -> Vec<Point> {
match pert {
Perturbation::Base { idx } => index(base_cases, *idx).to_owned(),
Perturbation::Point {
perturbation,
idx,
next,
} => {
let mut next = realize_perturbation(base_cases, next);
let p = index_mut(&mut next, *idx);
*p = perturbation.apply(*p);
next
}
Perturbation::Subdivision { t, idx, next } => {
let mut next = realize_perturbation(base_cases, next);
let idx = *idx % next.len();
let p0 = *index(&next, idx);
let p1 = *index(&next, idx + 1);
next.insert(idx + 1, p0.affine(&p1, *t));
next
}
Perturbation::Superimposition { left, right } => {
let mut next = realize_perturbation(base_cases, left);
next.extend(realize_perturbation(base_cases, right));
next
}
}
}
+561
View File
@@ -0,0 +1,561 @@
//! An algorithm for laying out curves in a given order.
//!
//! Our boolean ops algorithms work by first deciding horizontal orderings for curves
//! (with some guarantees about being approximately correct)
//! and then laying out the curves in a way that has the horizontal orders we decided
//! on. For this second step, which is the task of this module, we really
//! guarantee correctness: the curves that we output really have the specified
//! orders.
use std::collections::BinaryHeap;
use kurbo::{BezPath, CubicBez, ParamCurve};
use polycool::Quadratic;
use crate::{
SegIdx, Segment, Segments,
curve::{
Order, QuadraticApprox, solve_t_for_y,
transverse::{transversal_after, transversal_before},
y_subsegment,
},
num::CheapOrderedFloat,
order::ComparisonCache,
topology::{HalfOutputSegVec, OutputSegIdx, OutputSegVec, ScanLineOrder},
};
// TODO: maybe a Context type to keep the parameters in check?
#[allow(clippy::too_many_arguments)]
fn ordered_curves_all_close(
segs: &Segments,
order: &[SegIdx],
output_order: &[OutputSegIdx],
out: &mut OutputSegVec<PositionedOutputSeg>,
mut y0: f64,
y1: f64,
endpoints: &HalfOutputSegVec<kurbo::Point>,
accuracy: f64,
) {
if order.iter().all(|&s| segs[s].is_line()) {
// If everything is a line, since our endpoints are already in the right order
// the segments between them are also guaranteed to be in the right order.
return;
}
if order.len() == 2 {
let s0 = order[0];
let s1 = order[1];
let o0 = output_order[0];
let o1 = output_order[1];
let p0 = bez_end(&out[o0].path);
let p1 = bez_end(&out[o1].path);
let q0 = endpoints[o0.second_half()];
let q1 = endpoints[o1.second_half()];
if p0.y == y0 && p1.y == y0 {
let c0 = next_subsegment(&segs[s0], &out[o0].path, y1, q0);
let c1 = next_subsegment(&segs[s1], &out[o1].path, y1, q1);
if transversal_after(c0, c1, y1) {
return;
}
}
if q0.y == y1 && q1.y == y1 {
let c0 = next_subsegment(&segs[s0], &out[o0].path, y1, q0);
let c1 = next_subsegment(&segs[s1], &out[o1].path, y1, q1);
if transversal_before(c0, c1, y0) {
return;
}
}
}
// Ensure everything in `out` goes up to `y0`. Anything that doesn't go up to `y0` is
// an output where we can just copy from the input.
for (&seg_idx, &out_idx) in order.iter().zip(output_order) {
let out = &mut out[out_idx];
if bez_end_y(&out.path) < y0 {
out.copied_idx = Some(out.path.elements().len() - 1);
let endpoint = endpoints[out_idx.second_half()];
let c = next_subsegment(&segs[seg_idx], &out.path, y0, endpoint);
if segs[seg_idx].is_line() {
out.path.line_to(c.p3)
} else {
out.path.curve_to(c.p1, c.p2, c.p3);
}
}
}
let mut approxes = Vec::new();
// The recentering helps, but there are still some issues with approximation when almost horizontal
while y0 < y1 {
let next_y0 = approximate(y0, y1, order, segs, accuracy, &mut approxes);
let y_mid = (y0 + next_y0) / 2.0;
let mut x_mid_max_so_far = f64::NEG_INFINITY;
let mut x1_max_so_far = f64::NEG_INFINITY;
for (quad, out_idx) in approxes.iter().zip(output_order) {
let &[c0, c1, c2] = quad.q.coeffs();
// These would be the control points, but since `approximate` recenters the y interval,
// they're different...
// let x_mid = c0 + y_mid * c1 + y0 * next_y0 * c2;
// let x1 = c0 + c1 * next_y0 + c2 * next_y0 * next_y0;
let x_mid = c0 + (y0 - y_mid) * (next_y0 - y_mid) * c2;
let x1 = c0 + c1 * (next_y0 - y_mid) + c2 * (next_y0 - y_mid) * (next_y0 - y_mid);
x_mid_max_so_far = x_mid_max_so_far.max(x_mid);
x1_max_so_far = x1_max_so_far.max(x1);
out[*out_idx]
.path
.quad_to((x_mid_max_so_far, y_mid), (x1_max_so_far, next_y0));
}
approxes.clear();
y0 = next_y0;
}
}
fn horizontal_error_weight(c: CubicBez) -> f64 {
let tangents = [c.p1 - c.p0, c.p2 - c.p1, c.p3 - c.p2];
let mut weight = 0.0f64;
fn different_signs(x: f64, y: f64) -> bool {
(x > 0.0 && y < 0.0) || (x < 0.0) && (y > 0.0)
}
if different_signs(tangents[0].x, tangents[1].x)
|| different_signs(tangents[1].x, tangents[2].x)
{
return 1.0;
}
for v in tangents {
let denom = v.hypot2().sqrt();
if denom != 0.0 {
let w = v.y.abs() / denom;
weight = weight.max(w);
}
}
weight
}
// Pushes approximating parabolas into `out`, and returns the final y position.
fn approximate(
y0: f64,
mut y1: f64,
order: &[SegIdx],
segs: &Segments,
accuracy: f64,
out: &mut Vec<QuadraticApprox>,
) -> f64 {
let orig_len = out.len();
'retry: loop {
let y_mid = (y0 + y1) / 2.0;
for seg_idx in order {
let cubic = segs[*seg_idx].to_kurbo_cubic();
let mut cubic = y_subsegment(cubic, y0, y1);
cubic.p0.y -= y_mid;
cubic.p1.y -= y_mid;
cubic.p2.y -= y_mid;
cubic.p3.y -= y_mid;
let approx = QuadraticApprox::from_cubic(cubic, None);
let factor = horizontal_error_weight(cubic);
let too_short = y1 <= y0 + accuracy;
let accurate = approx.dmax.max(-approx.dmin) * factor < accuracy;
if !too_short && !accurate {
// TODO: can we be smarter about this?
y1 = (y0 + y1) / 2.0;
out.truncate(orig_len);
continue 'retry;
}
out.push(approx);
}
return y1;
}
}
#[derive(Debug, PartialEq, Eq)]
struct HeapEntry {
y: CheapOrderedFloat,
idx: OutputSegIdx,
}
impl Ord for HeapEntry {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
other.y.cmp(&self.y).then(self.idx.cmp(&other.idx))
}
}
impl PartialOrd for HeapEntry {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
fn bez_end(path: &BezPath) -> kurbo::Point {
match path.elements().last().unwrap() {
kurbo::PathEl::MoveTo(p)
| kurbo::PathEl::LineTo(p)
| kurbo::PathEl::QuadTo(_, p)
| kurbo::PathEl::CurveTo(_, _, p) => *p,
kurbo::PathEl::ClosePath => unreachable!(),
}
}
fn bez_end_y(path: &BezPath) -> f64 {
bez_end(path).y
}
fn next_subsegment(seg: &Segment, out: &BezPath, y1: f64, endpoint: kurbo::Point) -> CubicBez {
let p0 = bez_end(out);
let mut c = y_subsegment(seg.to_kurbo_cubic(), p0.y, y1);
c.p0 = p0;
if endpoint.y == y1 {
c.p3 = endpoint;
}
c
}
/// A positioned output segment, possibly perturbed from the original.
///
/// We perturb output segments in order to strictly enforce ordering.
/// A single segment might be expanded into a longer path.
#[derive(Default, Debug)]
pub struct PositionedOutputSeg {
/// The path, which is guaranteed to start and end at the same points as the
/// output segment did.
pub path: BezPath,
/// Sometimes (often, even) a large part of the output segment didn't need
/// to be perturbed. Our sweep-line invariants guarantee that unperturbed
/// part can only be a single contiguous interval: this stores the index
/// of the unperturbed part within `path.segments()`.
pub copied_idx: Option<usize>,
}
impl PositionedOutputSeg {
/// Creates an output segment from a kurbo segment, truncated and perturbed at the endpoints.
pub fn from_seg(s: kurbo::PathSeg, start: kurbo::Point, end: kurbo::Point) -> Self {
if start.y == end.y {
// Horizontal segments are always straight.
PositionedOutputSeg::from_line((start, end))
} else {
match s {
kurbo::PathSeg::Line(_) => PositionedOutputSeg::from_line((start, end)),
kurbo::PathSeg::Quad(q) => PositionedOutputSeg::from_quad(q, start, end),
kurbo::PathSeg::Cubic(c) => PositionedOutputSeg::from_cubic(c, start, end),
}
}
}
pub fn from_line(ell: impl Into<kurbo::Line>) -> Self {
let mut path = BezPath::with_capacity(2);
let ell = ell.into();
path.move_to(ell.p0);
path.line_to(ell.p1);
PositionedOutputSeg {
path,
copied_idx: Some(0),
}
}
/// Creates an output segment from a cubic, truncated and perturbed at the endpoints.
///
/// The cubic is assumed to be monotonic in `y`. We assume that `p0.y < p3.y`, and
/// both must be in the `y`-range of the cubic. The cubic is truncated to be between
/// the heights `p0.y` and `p3.y`, and then perturbed horizontally so that it starts
/// exactly and at `p0` and ends exactly at `p3`.
pub fn from_cubic(c: kurbo::CubicBez, p0: kurbo::Point, p3: kurbo::Point) -> Self {
debug_assert!(p0.y >= c.p0.y);
debug_assert!(p3.y <= c.p3.y);
debug_assert!(p0.y < p3.y);
let t0 = solve_t_for_y(c, p0.y);
let t3 = solve_t_for_y(c, p3.y);
let mut c = c.subsegment(t0..t3);
let dx0 = p0.x - c.p0.x;
let dx3 = p3.x - c.p3.x;
let s1 = ((c.p1.y - p0.y) / (p3.y - p0.y)).clamp(0.0, 1.0);
let s2 = ((c.p2.y - p0.y) / (p3.y - p0.y)).clamp(0.0, 1.0);
c.p1.x += (1.0 - s1) * dx0 + s1 * dx3;
c.p2.x += (1.0 - s2) * dx0 + s2 * dx3;
let mut path = BezPath::with_capacity(2);
path.move_to(p0);
path.curve_to(c.p1, c.p2, p3);
PositionedOutputSeg {
path,
copied_idx: Some(0),
}
}
/// Creates an output segment from a quadratic, truncated and perturbed at the endpoints.
///
/// The cubic is assumed to be monotonic in `y`. We assume that `p0.y < p2.y`, and
/// both must be in the `y`-range of the cubic. The cubic is truncated to be between
/// the heights `p0.y` and `p2.y`, and then perturbed horizontally so that it starts
/// exactly and at `p0` and ends exactly at `p2`.
pub fn from_quad(q: kurbo::QuadBez, p0: kurbo::Point, p2: kurbo::Point) -> Self {
debug_assert!(p0.y >= q.p0.y);
debug_assert!(p2.y <= q.p2.y);
debug_assert!(p0.y < p2.y);
fn solve_t_for_y(q: kurbo::QuadBez, y: f64) -> f64 {
// Write the y coordinate of `q - y` as `at^2 + bt + c`.
let c = q.p0.y - y;
let b = 2.0 * (q.p1.y - q.p0.y);
let a = q.p0.y - 2.0 * q.p1.y + q.p2.y;
let roots = Quadratic::new([c, b, a]).roots();
for r in roots {
if (0.0..=1.0).contains(&r) {
return r;
}
}
// If we land here, y should be close to one of the endpoints and it
// was just numerical error that pushed the root out of [0.0, 1.0].
// The only question is which endpoint.
if y >= 0.5 * (q.p0.y + q.p2.y) {
1.0
} else {
0.0
}
}
let t0 = solve_t_for_y(q, p0.y);
let t2 = solve_t_for_y(q, p2.y);
let mut q = q.subsegment(t0..t2);
let dx0 = p0.x - q.p0.x;
let dx2 = p2.x - q.p2.x;
let s = ((q.p1.y - p0.y) / (p2.y - p0.y)).clamp(0.0, 1.0);
q.p1.x += (1.0 - s) * dx0 + s * dx2;
let mut path = BezPath::with_capacity(2);
path.move_to(p0);
path.quad_to(q.p1, p2);
PositionedOutputSeg {
path,
copied_idx: Some(0),
}
}
}
/// Compute positions for all of the output segments.
///
/// The orders between the output segments is specified by `order`. The endpoints
/// should have been already computed (in a way that satisfies the order), and
/// are provided in `endpoints`. For each output segment, we return a Bézier
/// path.
pub(crate) fn compute_positions(
segs: &Segments,
orig_seg_map: &OutputSegVec<SegIdx>,
cmp: &mut ComparisonCache,
endpoints: &HalfOutputSegVec<kurbo::Point>,
scan_order: &ScanLineOrder,
accuracy: f64,
) -> OutputSegVec<PositionedOutputSeg> {
let mut out = OutputSegVec::<PositionedOutputSeg>::with_size(orig_seg_map.len());
// We try to build `out` lazily, by avoiding copying input segments to outputs segments
// until they're needed (by copying in one go, we avoid excess subdivisions). That means
// we need to separately keep track of how far down we've looked at each output. If
// we weren't lazy, that would just be the last `y` coordinate in `out`.
let mut last_y = OutputSegVec::<f64>::with_size(orig_seg_map.len());
let mut queue = BinaryHeap::<HeapEntry>::new();
for idx in out.indices() {
let p = endpoints[idx.first_half()];
let q = endpoints[idx.second_half()];
if p.y == q.y {
out[idx].path.move_to(p);
out[idx].path.line_to(q);
out[idx].copied_idx = Some(0);
continue;
}
out[idx].path.move_to(p);
queue.push(HeapEntry { y: p.y.into(), idx });
}
while let Some(entry) = queue.pop() {
// Maybe this entry was already handled by one of its neighbors, in which case we skip it.
let y0 = entry.y.into_inner();
if last_y[entry.idx] > y0 {
continue;
}
let mut y1 = endpoints[entry.idx.second_half()].y;
let mut west_scan = vec![entry.idx];
let mut cur = entry.idx;
while let Some(nbr) = scan_order.west_neighbor_after(cur, y0) {
let order = cmp.compare_segments(segs, orig_seg_map[nbr], orig_seg_map[cur]);
let (_, cmp_end_y, cmp_order) = order.iter().find(|(_, end_y, _)| *end_y > y0).unwrap();
if cmp_order == Order::Left {
let next_close_y = scan_order
.close_west_neighbor_height_after(cur, y0, orig_seg_map, segs, cmp)
.unwrap_or(f64::INFINITY);
y1 = y1.min(next_close_y.max(cmp_end_y));
break;
} else {
y1 = y1.min(cmp_end_y).min(endpoints[nbr.second_half()].y);
west_scan.push(nbr);
}
cur = nbr;
}
let mut east_scan = vec![];
let mut cur = entry.idx;
while let Some(nbr) = scan_order.east_neighbor_after(cur, y0) {
let order = cmp.compare_segments(segs, orig_seg_map[cur], orig_seg_map[nbr]);
let (_, cmp_end_y, cmp_order) = order.iter().find(|(_, end_y, _)| *end_y > y0).unwrap();
if cmp_order == Order::Left {
let next_close_y = scan_order
.close_east_neighbor_height_after(cur, y0, orig_seg_map, segs, cmp)
.unwrap_or(f64::INFINITY);
y1 = y1.min(next_close_y.max(cmp_end_y));
break;
} else {
y1 = y1.min(cmp_end_y).min(endpoints[nbr.second_half()].y);
east_scan.push(nbr);
}
cur = nbr;
}
let mut neighbors = west_scan;
neighbors.reverse();
neighbors.extend(east_scan);
if neighbors.len() == 1 {
let idx = entry.idx;
// We're far from everything, so we can just copy the input bezier to the output.
// We only actually do this eagerly for horizontal segments: for other segments
// we'll hold off on the copying because it might allow us to avoid further
// subdivision.
if y0 == y1 {
out[idx].copied_idx = Some(out[idx].path.elements().len() - 1);
out[idx].path.line_to(endpoints[idx.second_half()]);
}
} else {
let orig_neighbors = neighbors
.iter()
.map(|idx| orig_seg_map[*idx])
.collect::<Vec<_>>();
ordered_curves_all_close(
segs,
&orig_neighbors,
&neighbors,
&mut out,
y0,
y1,
endpoints,
accuracy,
);
}
for idx in neighbors {
let y_end = endpoints[idx.second_half()].y;
if y1 < y_end {
queue.push(HeapEntry { y: y1.into(), idx });
}
last_y[idx] = y1;
}
}
for out_idx in out.indices() {
let out = &mut out[out_idx];
let endpoint = endpoints[out_idx.second_half()];
if bez_end_y(&out.path) < endpoint.y {
let seg_idx = orig_seg_map[out_idx];
out.copied_idx = Some(out.path.elements().len() - 1);
if segs[seg_idx].is_line() {
out.path.line_to(endpoint)
} else {
let c = next_subsegment(&segs[seg_idx], &out.path, endpoint.y, endpoint);
out.path.curve_to(c.p1, c.p2, c.p3);
}
} else {
// The quadratic approximations don't respect the fixed endpoints, so tidy them
// up. Since both the quadratic approximations and the endpoints satisfy
// the ordering, this doesn't mess up the ordering.
match out.path.elements_mut().last_mut().unwrap() {
kurbo::PathEl::MoveTo(p)
| kurbo::PathEl::LineTo(p)
| kurbo::PathEl::QuadTo(_, p)
| kurbo::PathEl::CurveTo(_, _, p) => {
*p = endpoints[out_idx.second_half()];
}
kurbo::PathEl::ClosePath => unreachable!(),
}
}
}
out
}
#[cfg(test)]
mod tests {
use kurbo::{BezPath, CubicBez, Line, PathSeg};
use crate::Segments;
use super::horizontal_error_weight;
#[test]
fn error_weight() {
// Vertical lines have a weight of 1.
let c: CubicBez = PathSeg::from(Line::new((0.0, 0.0), (0.0, 1.0))).to_cubic();
assert_eq!(horizontal_error_weight(c), 1.0);
// Horizontal lines have a weight of 0.
let c: CubicBez = PathSeg::from(Line::new((1.0, 0.0), (0.0, 0.0))).to_cubic();
assert_eq!(horizontal_error_weight(c), 0.0);
let c: CubicBez = PathSeg::from(Line::new((0.0, 0.0), (1.0, 0.0))).to_cubic();
assert_eq!(horizontal_error_weight(c), 0.0);
// S-shaped curves have a weight of 1
let c = CubicBez::new((0.0, 0.0), (1.0, 0.0), (-1.0, 1.0), (0.0, 1.0));
assert_eq!(horizontal_error_weight(c), 1.0);
// Diagonal lines have a weight of about 1/sqrt(2).
let c: CubicBez = PathSeg::from(Line::new((0.0, 0.0), (1.0, 1.0))).to_cubic();
assert_eq!(horizontal_error_weight(c), 1.0 / 2.0f64.sqrt());
}
// Here's an example of lines where we used to have a bad
// approximation. Check that they only require a single
// approximation step.
#[test]
fn linear_approx() {
let mut p0 = BezPath::new();
p0.move_to((-212.00062561035156, 90.03582000732422));
p0.line_to((211.99937438964844, 90.03646087646484));
p0.close_path();
let mut p1 = BezPath::new();
p1.move_to((211.99964904785153, -90.0358200073242));
p1.line_to((211.99937438964844, 90.03646087646484));
p1.close_path();
let mut segs = Segments::default();
segs.add_bez_path(&p0).unwrap();
segs.add_bez_path(&p1).unwrap();
let s0 = segs.indices().next().unwrap();
let s1 = segs.indices().nth(2).unwrap();
let mut out = Vec::new();
let y1 = 90.03646087646484;
let next_y0 = super::approximate(90.03645987646233, y1, &[s0, s1], &segs, 2.5e-7, &mut out);
assert_eq!(next_y0, y1);
}
}
+482
View File
@@ -0,0 +1,482 @@
use std::iter::Peekable;
use kurbo::BezPath;
use crate::{
curve::AlmostHorizontalDecomposition,
geom::{Point, Segment, monotonic_pieces},
num::CheapOrderedFloat,
};
/// An index into our segment arena.
///
/// Throughout this library, we assign identities to segments, so that we may
/// consider segments as different even if they have the same start- and end-points.
///
/// This index is used to identify a segment, whose data can be retrieved by looking
/// it up in [`Segments`]. (Of course, this index-as-identifier breaks down if there are
/// multiple `Segments` in flight. Just be careful not to mix them up.)
#[cfg_attr(test, derive(serde::Serialize))]
#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct SegIdx(pub(crate) usize);
/// A vector indexed by `SegIdx`.
#[cfg_attr(test, derive(serde::Serialize))]
#[derive(Clone, Hash, PartialEq, Eq)]
#[cfg_attr(test, serde(transparent))]
pub struct SegVec<T> {
inner: Vec<T>,
}
impl_typed_vec!(SegVec, SegIdx, "s");
/// An arena of segments, each of which is a cubic Bézier.
///
/// Segments are indexed by [`SegIdx`] and can be retrieved by indexing (i.e. with square brackets).
#[derive(Clone, Default)]
pub struct Segments {
segs: SegVec<Segment>,
horizontal_decomps: SegVec<AlmostHorizontalDecomposition>,
contour_prev: SegVec<Option<SegIdx>>,
contour_next: SegVec<Option<SegIdx>>,
/// For each segment, stores true if the sweep-line order (small y to big y)
/// is the same as the orientation in its original contour.
orientation: SegVec<bool>,
/// For each segment, stores its parameter range in the original input
/// segment (which may have been split into monotonic sub-segments). Keeping
/// track of where the splits happened allows us to potentially merge things
/// back at the end.
pub(crate) split_from_predecessor: SegVec<(f64, f64)>,
/// The original input segments, for reconstructing/merging. (TODO: this
/// representation is wasteful, since we only need it for segments that
/// got split.)
pub(crate) input_segs: SegVec<kurbo::PathSeg>,
/// All the entrance heights, of segments, ordered by height.
/// This includes horizontal segments.
enter: Vec<(f64, SegIdx)>,
/// All the exit heights of segments, ordered by height.
/// This does not include horizontal segments.
exit: Vec<(f64, SegIdx)>,
}
/// Certain operations expect closed paths, and return this as an error when a path is not closed.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct NonClosedPath {
/// The point at the start of the non-closed path (always generated by a [`kurbo::PathEl::MoveTo`]).
pub start_point: kurbo::Point,
/// The point at the end of the non-closed path.
pub end_point: kurbo::Point,
}
impl std::fmt::Display for NonClosedPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"non-closed path starting at {} and ending at {}",
self.start_point, self.end_point,
)
}
}
impl std::error::Error for NonClosedPath {}
struct SegmentEntryFormatter<'a> {
idx: SegIdx,
seg: &'a Segment,
prev: Option<SegIdx>,
next: Option<SegIdx>,
oriented: bool,
}
impl std::fmt::Debug for SegmentEntryFormatter<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let seg_idx = self.idx;
let seg = self.seg;
let prefix = if self.oriented {
self.prev.map(|i| format!("{i:?} -> ")).unwrap_or_default()
} else {
self.next.map(|i| format!("{i:?} <- ")).unwrap_or_default()
};
let suffix = if self.oriented {
self.next.map(|i| format!(" -> {i:?}")).unwrap_or_default()
} else {
self.prev.map(|i| format!(" <- {i:?}")).unwrap_or_default()
};
write!(f, "{seg_idx:?}: {prefix}{seg:?}{suffix}")
}
}
impl std::fmt::Debug for Segments {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut list = f.debug_list();
for (idx, seg) in self.segs.iter() {
list.entry(&SegmentEntryFormatter {
idx,
seg,
prev: self.contour_prev[idx],
next: self.contour_next[idx],
oriented: self.orientation[idx],
});
}
list.finish()
}
}
fn cyclic_pairs<T>(xs: &[T]) -> impl Iterator<Item = (&T, &T)> {
pairs(xs).chain(xs.last().zip(xs.first()))
}
fn pairs<T>(xs: &[T]) -> impl Iterator<Item = (&T, &T)> {
xs.windows(2).map(|pair| (&pair[0], &pair[1]))
}
struct SubpathIter<'a, I: Iterator> {
inner: &'a mut Peekable<I>,
started: bool,
}
impl<I> Iterator for SubpathIter<'_, I>
where
I: Iterator<Item = kurbo::PathEl>,
{
type Item = kurbo::PathEl;
fn next(&mut self) -> Option<Self::Item> {
let ret = self.inner.peek()?;
if matches!(ret, kurbo::PathEl::MoveTo(_)) && self.started {
None
} else {
self.started = true;
self.inner.next()
}
}
}
struct Subpaths<I: Iterator> {
inner: Peekable<I>,
}
impl<I> Subpaths<I>
where
I: Iterator<Item = kurbo::PathEl>,
{
fn next(&mut self) -> Option<SubpathIter<'_, I>> {
if self.inner.peek().is_none() {
None
} else {
Some(SubpathIter {
inner: &mut self.inner,
started: false,
})
}
}
}
impl Segments {
/// The number of segments in this arena.
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.segs.len()
}
/// Iterate over all indices that can be used to index into this arena.
pub fn indices(&self) -> impl Iterator<Item = SegIdx> + use<> {
(0..self.segs.len()).map(SegIdx)
}
/// Iterate over all segments in this arena.
pub fn segments(&self) -> impl Iterator<Item = &Segment> {
self.segs.iter().map(|(_, s)| s)
}
/// Returns the starting point of the segment at `idx`, relative to the segment's original orientation.
///
/// The segment itself is stored in sweep-line order (i.e. its starting
/// point has the smaller y coordinate) regardless of the original
/// orientation of the segment. Use this method to retrieve the segment's
/// original start point.
pub fn oriented_start(&self, idx: SegIdx) -> Point {
if self.orientation[idx] {
self[idx].start()
} else {
self[idx].end()
}
}
/// Returns the ending point of the segment at `idx`, relative to the segment's original orientation.
///
/// The segment itself is stored in sweep-line order (i.e. its starting
/// point has the smaller y coordinate) regardless of the original
/// orientation of the segment. Use this method to retrieve the segment's
/// original end point.
pub fn oriented_end(&self, idx: SegIdx) -> Point {
if self.orientation[idx] {
self[idx].end()
} else {
self[idx].start()
}
}
/// Returns the index of the segment following `idx`.
///
/// If `idx` is part of a non-closed path and it is the last segment,
/// this returns `None`. If `idx` is part of a closed path, this will
/// always return `Some`, and you might need to be careful to avoid looping
/// infinitely.
pub fn contour_next(&self, idx: SegIdx) -> Option<SegIdx> {
self.contour_next[idx]
}
/// Returns the index of the segment preceding `idx`.
///
/// If `idx` is part of a non-closed path and it is the first segment,
/// this returns `None`. If `idx` is part of a closed path, this will
/// always return `Some`, and you might need to be careful to avoid looping
/// infinitely.
pub fn contour_prev(&self, idx: SegIdx) -> Option<SegIdx> {
self.contour_prev[idx]
}
/// Does the sweep-line orientation of `idx` agree with its original orientation?
pub fn positively_oriented(&self, idx: SegIdx) -> bool {
self.orientation[idx]
}
/// Add a (non-closed) polyline to this arena.
pub fn add_points<P: Into<Point>>(&mut self, ps: impl IntoIterator<Item = P>) {
let old_len = self.segs.len();
let ps: Vec<_> = ps.into_iter().map(|p| p.into()).collect();
if ps.len() <= 1 {
return;
}
for (p, q) in pairs(&ps) {
let (a, b, orient) = if p < q { (p, q, true) } else { (q, p, false) };
self.push_seg(Segment::straight(*a, *b));
self.orientation.push(orient);
self.split_from_predecessor.push((0.0, 1.0));
self.input_segs
.push(kurbo::PathSeg::Line((p.to_kurbo(), q.to_kurbo()).into()));
self.contour_prev
.push(Some(SegIdx(self.segs.len().saturating_sub(2))));
self.contour_next.push(Some(SegIdx(self.segs.len())));
}
if old_len < self.segs.len() {
self.contour_prev[SegIdx(old_len)] = None;
// unwrap: contour_next has the same length as `segs`, which is
// non-empty because we checked its length
*self.contour_next.inner.last_mut().unwrap() = None;
}
self.update_enter_exit(old_len);
}
/// Add a collection of closed polylines to this arena.
///
/// This can be much faster than calling `add_cycles` repeatedly.
pub fn add_closed_polylines<P: Into<Point>>(
&mut self,
ps: impl IntoIterator<Item = impl IntoIterator<Item = P>>,
) {
let old_len = self.segs.len();
for p in ps {
self.add_polyline_without_updating_enter_exit(p);
}
self.update_enter_exit(old_len);
}
/// Add a closed polyline to this arena.
pub fn add_closed_polyline<P: Into<Point>>(&mut self, ps: impl IntoIterator<Item = P>) {
let old_len = self.segs.len();
self.add_polyline_without_updating_enter_exit(ps);
self.update_enter_exit(old_len);
}
/// Add a Bézier path to this arena.
///
/// The path can contain multiple subpaths, and each of them must be closed.
pub fn add_bez_path(&mut self, p: &BezPath) -> Result<(), NonClosedPath> {
let old_len = self.segs.len();
self.add_path_without_updating_enter_exit(p, true)?;
self.update_enter_exit(old_len);
Ok(())
}
/// Add a Bézier path to this arena.
///
/// The path can contain multiple subpaths, and each of them may or may not be closed.
pub fn add_non_closed_bez_path(&mut self, p: &BezPath) -> Result<(), NonClosedPath> {
let old_len = self.segs.len();
self.add_path_without_updating_enter_exit(p, false)?;
self.update_enter_exit(old_len);
Ok(())
}
pub(crate) fn add_path_without_updating_enter_exit(
&mut self,
p: &BezPath,
must_be_closed: bool,
) -> Result<(), NonClosedPath> {
let mut subpaths = Subpaths {
inner: p.iter().peekable(),
};
while let Some(subpath) = subpaths.next() {
let old_len = self.segs.len();
for seg in kurbo::segments(subpath) {
let pieces = monotonic_pieces(seg);
for monotonic in pieces {
self.push_seg(monotonic.piece);
self.orientation.push(monotonic.positively_oriented);
self.split_from_predecessor
.push((monotonic.start_t, monotonic.end_t));
self.input_segs.push(seg);
self.contour_prev
.push(Some(SegIdx(self.segs.len().saturating_sub(2))));
self.contour_next.push(Some(SegIdx(self.segs.len())));
}
}
if old_len < self.segs.len() {
let start_idx = SegIdx(old_len);
let end_idx = SegIdx(self.segs.len() - 1);
// unwrap: contour_next has the same length as `segs`, which is
// non-empty because we checked its length
let start_point = self.oriented_start(start_idx).to_kurbo();
let end_point = self.oriented_end(end_idx).to_kurbo();
if start_point == end_point {
self.contour_prev[start_idx] = Some(end_idx);
self.contour_next[end_idx] = Some(start_idx);
} else if must_be_closed {
return Err(NonClosedPath {
start_point,
end_point,
});
} else {
self.contour_prev[start_idx] = None;
self.contour_next[end_idx] = None;
}
}
}
Ok(())
}
fn add_polyline_without_updating_enter_exit<P: Into<Point>>(
&mut self,
ps: impl IntoIterator<Item = P>,
) {
let old_len = self.segs.len();
let ps: Vec<_> = ps.into_iter().map(|p| p.into()).collect();
if ps.len() <= 1 {
return;
}
for (p, q) in cyclic_pairs(&ps) {
if p == q {
continue;
}
let (a, b, orient) = if p < q { (p, q, true) } else { (q, p, false) };
self.push_seg(Segment::straight(*a, *b));
self.orientation.push(orient);
self.split_from_predecessor.push((0.0, 1.0));
self.input_segs
.push(kurbo::PathSeg::Line((p.to_kurbo(), q.to_kurbo()).into()));
self.contour_prev
.push(Some(SegIdx(self.segs.len().saturating_sub(2))));
self.contour_next.push(Some(SegIdx(self.segs.len())));
}
if old_len < self.segs.len() {
self.contour_prev[SegIdx(old_len)] = Some(SegIdx(self.segs.len() - 1));
// unwrap: contour_next has the same length as `segs`, which is
// non-empty because we checked its length
*self.contour_next.inner.last_mut().unwrap() = Some(SegIdx(old_len));
}
}
fn push_seg(&mut self, seg: Segment) {
let decomp = if seg.is_horizontal() {
AlmostHorizontalDecomposition::default()
} else {
AlmostHorizontalDecomposition::from_monotone_cubic(seg.to_kurbo_cubic())
};
self.horizontal_decomps.push(decomp);
self.segs.push(seg);
}
/// Construct a segment arena from a single closed polyline.
pub fn from_closed_polyline<P: Into<Point>>(ps: impl IntoIterator<Item = P>) -> Self {
let mut ret = Self::default();
ret.add_closed_polyline(ps);
ret
}
pub(crate) fn update_enter_exit(&mut self, old_len: usize) {
for idx in old_len..self.len() {
let seg_idx = SegIdx(idx);
let seg = &self.segs[seg_idx];
self.enter.push((seg.start().y, seg_idx));
if !seg.is_horizontal() {
self.exit.push((seg.end().y, seg_idx));
}
}
// We sort the enter segments by y position, and then by horizontal
// start position so that they're fairly likely to get inserted in the
// sweep-line in order (which makes the indexing fix-ups faster).
self.enter.sort_by(|(y1, seg1), (y2, seg2)| {
CheapOrderedFloat::from(*y1)
.cmp(&CheapOrderedFloat::from(*y2))
.then_with(|| {
CheapOrderedFloat::from(self.segs[*seg1].at_y(*y1))
.cmp(&CheapOrderedFloat::from(self.segs[*seg2].at_y(*y1)))
})
});
self.exit.sort_by(|(y1, _), (y2, _)| {
CheapOrderedFloat::from(*y1).cmp(&CheapOrderedFloat::from(*y2))
});
}
/// All the entrance heights of segments, ordered by height.
///
/// Includes horizontal segments.
pub fn entrances(&self) -> &[(f64, SegIdx)] {
&self.enter
}
/// All the exit heights of segments, ordered by height.
///
/// Does not include horizontal segments.
pub fn exits(&self) -> &[(f64, SegIdx)] {
&self.exit
}
/// Checks that we satisfy our internal invariants. For testing only.
pub fn check_invariants(&self) {
for (idx, seg) in self.segs.iter() {
assert!(seg.start() <= seg.end());
assert_ne!(seg.start(), seg.end());
if let Some(next_idx) = self.contour_next(idx) {
assert_eq!(self.oriented_end(idx), self.oriented_start(next_idx));
assert_eq!(self.contour_prev(next_idx), Some(idx));
}
}
}
pub(crate) fn horiz_decomp(&self, idx: SegIdx) -> &AlmostHorizontalDecomposition {
debug_assert!(!self.segs[idx].is_horizontal());
&self.horizontal_decomps[idx]
}
}
impl std::ops::Index<SegIdx> for Segments {
type Output = Segment;
fn index(&self, index: SegIdx) -> &Self::Output {
&self.segs[index]
}
}
@@ -0,0 +1,39 @@
---
source: src/lib.rs
expression: output
---
Contours(
contours: [
Contour(
path: BezPath([
MoveTo(Point(
x: 0.0,
y: 10.0,
)),
LineTo(Point(
x: 0.0,
y: 0.0,
)),
LineTo(Point(
x: 10.0,
y: 0.0,
)),
LineTo(Point(
x: 10.0,
y: 1.0,
)),
LineTo(Point(
x: 10.0,
y: 10.0,
)),
LineTo(Point(
x: 0.0,
y: 10.0,
)),
ClosePath,
]),
parent: None,
outer: true,
),
],
)
@@ -0,0 +1,35 @@
---
source: src/lib.rs
expression: output
---
Contours(
contours: [
Contour(
path: BezPath([
MoveTo(Point(
x: 0.0,
y: 0.5,
)),
LineTo(Point(
x: 0.0,
y: 0.0,
)),
LineTo(Point(
x: 0.5,
y: 0.0,
)),
LineTo(Point(
x: 0.5,
y: 0.5,
)),
LineTo(Point(
x: 0.0,
y: 0.5,
)),
ClosePath,
]),
parent: None,
outer: true,
),
],
)
@@ -0,0 +1,225 @@
---
source: src/topology.rs
expression: top
---
Topology(
eps: 0.01,
correctness: GoodEnough,
tag: [
A,
A,
A,
A,
],
open_segs: [
[],
[],
[],
[],
],
winding: OutputSegVec(
inner: [
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
),
],
),
points: PointVec(
inner: [
Point(
y: 0.0,
x: 0.0,
),
Point(
y: 1.0,
x: -1.0,
),
Point(
y: 1.0,
x: 1.0,
),
Point(
y: 2.0,
x: 0.0,
),
],
),
point_idx: HalfOutputSegVec(
start: [
PointIdx(0),
PointIdx(0),
PointIdx(1),
PointIdx(2),
],
end: [
PointIdx(1),
PointIdx(2),
PointIdx(3),
PointIdx(3),
],
),
point_neighbors: HalfOutputSegVec(
start: [
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: false,
),
),
],
end: [
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: false,
),
),
],
),
deleted: OutputSegVec(
inner: [
false,
false,
false,
false,
],
),
scan_west: OutputSegVec(
inner: [
None,
Some(OutputSegIdx(0)),
None,
Some(OutputSegIdx(2)),
],
),
scan_east: OutputSegVec(
inner: [
None,
None,
None,
None,
],
),
scan_after: [],
orig_seg: OutputSegVec(
inner: [
SegIdx(3),
SegIdx(0),
SegIdx(2),
SegIdx(1),
],
),
positively_oriented: OutputSegVec(
inner: [
false,
true,
false,
true,
],
),
)
@@ -0,0 +1,596 @@
---
source: src/topology.rs
expression: "(top, contours)"
---
(Topology(
eps: 0.01,
correctness: GoodEnough,
tag: [
A,
A,
A,
A,
B,
B,
B,
B,
B,
B,
],
open_segs: [
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
],
winding: OutputSegVec(
inner: [
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: -1,
),
clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: -1,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: -1,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: -2,
),
clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: -1,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: -1,
),
clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: -2,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: -1,
),
clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: -2,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
),
],
),
points: PointVec(
inner: [
Point(
y: -2.0,
x: -2.0,
),
Point(
y: -2.0,
x: 2.0,
),
Point(
y: -1.0,
x: -1.5,
),
Point(
y: -1.0,
x: 1.5,
),
Point(
y: 0.0,
x: -0.1,
),
Point(
y: 0.0,
x: 0.1,
),
Point(
y: 2.0,
x: -2.0,
),
Point(
y: 2.0,
x: 0.0,
),
Point(
y: 2.0,
x: 2.0,
),
],
),
point_idx: HalfOutputSegVec(
start: [
PointIdx(0),
PointIdx(0),
PointIdx(1),
PointIdx(2),
PointIdx(2),
PointIdx(3),
PointIdx(4),
PointIdx(4),
PointIdx(5),
PointIdx(6),
PointIdx(7),
],
end: [
PointIdx(6),
PointIdx(1),
PointIdx(8),
PointIdx(7),
PointIdx(3),
PointIdx(7),
PointIdx(7),
PointIdx(5),
PointIdx(7),
PointIdx(7),
PointIdx(8),
],
),
point_neighbors: HalfOutputSegVec(
start: [
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(7),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(7),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(6),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(6),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(7),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(7),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(9),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(5),
first_half: false,
),
),
],
end: [
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(9),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(9),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(10),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(10),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(6),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(9),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(5),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(5),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(10),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(8),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(8),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(8),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(8),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(5),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(6),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(10),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: false,
),
),
],
),
deleted: OutputSegVec(
inner: [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
],
),
scan_west: OutputSegVec(
inner: [
None,
Some(OutputSegIdx(0)),
Some(OutputSegIdx(0)),
Some(OutputSegIdx(0)),
Some(OutputSegIdx(3)),
Some(OutputSegIdx(3)),
Some(OutputSegIdx(3)),
Some(OutputSegIdx(6)),
Some(OutputSegIdx(6)),
None,
None,
],
),
scan_east: OutputSegVec(
inner: [
None,
None,
None,
None,
None,
Some(OutputSegIdx(2)),
None,
None,
Some(OutputSegIdx(5)),
None,
None,
],
),
scan_after: [],
orig_seg: OutputSegVec(
inner: [
SegIdx(3),
SegIdx(0),
SegIdx(1),
SegIdx(4),
SegIdx(6),
SegIdx(5),
SegIdx(7),
SegIdx(9),
SegIdx(8),
SegIdx(2),
SegIdx(2),
],
),
positively_oriented: OutputSegVec(
inner: [
false,
true,
true,
true,
false,
false,
true,
false,
false,
false,
false,
],
),
), Contours(
contours: [
Contour(
path: BezPath([
MoveTo(Point(
x: -2.0,
y: 2.0,
)),
LineTo(Point(
x: -2.0,
y: -2.0,
)),
LineTo(Point(
x: 2.0,
y: -2.0,
)),
LineTo(Point(
x: 2.0,
y: 2.0,
)),
LineTo(Point(
x: 0.0,
y: 2.0,
)),
LineTo(Point(
x: -2.0,
y: 2.0,
)),
ClosePath,
]),
parent: None,
outer: true,
),
Contour(
path: BezPath([
MoveTo(Point(
x: 0.0,
y: 2.0,
)),
LineTo(Point(
x: 1.5,
y: -1.0,
)),
LineTo(Point(
x: -1.5,
y: -1.0,
)),
LineTo(Point(
x: 0.0,
y: 2.0,
)),
ClosePath,
]),
parent: Some(ContourIdx(0)),
outer: false,
),
Contour(
path: BezPath([
MoveTo(Point(
x: 0.0,
y: 2.0,
)),
LineTo(Point(
x: -0.1,
y: 0.0,
)),
LineTo(Point(
x: 0.1,
y: 0.0,
)),
LineTo(Point(
x: 0.0,
y: 2.0,
)),
ClosePath,
]),
parent: Some(ContourIdx(1)),
outer: true,
),
],
))
@@ -0,0 +1,313 @@
---
source: src/topology.rs
expression: "(top, contours)"
---
(Topology(
eps: 0.000001,
correctness: GoodEnough,
tag: [
(),
(),
(),
(),
(),
(),
],
open_segs: [
[],
[],
[],
[],
[],
[],
],
winding: OutputSegVec(
inner: [
HalfSegmentWindingNumbers(
counter_clockwise: 1,
clockwise: 0,
),
HalfSegmentWindingNumbers(
counter_clockwise: 0,
clockwise: 1,
),
HalfSegmentWindingNumbers(
counter_clockwise: 1,
clockwise: 0,
),
HalfSegmentWindingNumbers(
counter_clockwise: 1,
clockwise: 0,
),
HalfSegmentWindingNumbers(
counter_clockwise: 0,
clockwise: 1,
),
HalfSegmentWindingNumbers(
counter_clockwise: 1,
clockwise: 0,
),
],
),
points: PointVec(
inner: [
Point(
y: -0.2886751345948129,
x: -0.769800358919501,
),
Point(
y: 0.0,
x: -1.0,
),
Point(
y: 0.0,
x: 1.0,
),
Point(
y: 0.28867513459481287,
x: 0.7698003589195007,
),
Point(
y: 4.0,
x: -2.0,
),
Point(
y: 4.0,
x: 2.0,
),
],
),
point_idx: HalfOutputSegVec(
start: [
PointIdx(0),
PointIdx(0),
PointIdx(1),
PointIdx(2),
PointIdx(2),
PointIdx(4),
],
end: [
PointIdx(1),
PointIdx(3),
PointIdx(4),
PointIdx(3),
PointIdx(5),
PointIdx(5),
],
),
point_neighbors: HalfOutputSegVec(
start: [
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: false,
),
),
],
end: [
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(5),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(5),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(5),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(5),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: false,
),
),
],
),
deleted: OutputSegVec(
inner: [
false,
false,
false,
false,
false,
false,
],
),
scan_west: OutputSegVec(
inner: [
None,
Some(OutputSegIdx(0)),
None,
Some(OutputSegIdx(1)),
Some(OutputSegIdx(3)),
None,
],
),
scan_east: OutputSegVec(
inner: [
None,
None,
Some(OutputSegIdx(1)),
None,
None,
None,
],
),
scan_after: [
(0.28867513459481287, Some(OutputSegIdx(2)), Some(OutputSegIdx(4))),
],
orig_seg: OutputSegVec(
inner: [
SegIdx(0),
SegIdx(1),
SegIdx(5),
SegIdx(2),
SegIdx(3),
SegIdx(4),
],
),
positively_oriented: OutputSegVec(
inner: [
false,
true,
false,
false,
true,
false,
],
),
), Contours(
contours: [
Contour(
path: BezPath([
MoveTo(Point(
x: -1.0,
y: 0.0,
)),
CurveTo(Point(
x: -1.0,
y: -1.0,
), Point(
x: 1.0,
y: 1.0,
), Point(
x: 1.0,
y: 0.0,
)),
LineTo(Point(
x: 2.0,
y: 4.0,
)),
LineTo(Point(
x: -2.0,
y: 4.0,
)),
LineTo(Point(
x: -1.0,
y: 0.0,
)),
ClosePath,
]),
parent: None,
outer: true,
),
],
))
@@ -0,0 +1,320 @@
---
source: src/topology.rs
expression: "(top, contours)"
---
(Topology(
eps: 0.000001,
correctness: GoodEnough,
tag: [
(),
(),
(),
(),
],
open_segs: [
[],
[],
[],
[],
],
winding: OutputSegVec(
inner: [
HalfSegmentWindingNumbers(
counter_clockwise: 1,
clockwise: 0,
),
HalfSegmentWindingNumbers(
counter_clockwise: 0,
clockwise: 1,
),
HalfSegmentWindingNumbers(
counter_clockwise: 1,
clockwise: 0,
),
HalfSegmentWindingNumbers(
counter_clockwise: -1,
clockwise: 0,
),
HalfSegmentWindingNumbers(
counter_clockwise: 0,
clockwise: -1,
),
HalfSegmentWindingNumbers(
counter_clockwise: 0,
clockwise: -1,
),
],
),
points: PointVec(
inner: [
Point(
y: -0.2886751345948129,
x: -0.769800358919501,
),
Point(
y: 0.0,
x: -1.0,
),
Point(
y: 0.0,
x: 0.00000000000000016653345369377348,
),
Point(
y: 0.0,
x: 1.0,
),
Point(
y: 0.28867513459481287,
x: 0.7698003589195007,
),
],
),
point_idx: HalfOutputSegVec(
start: [
PointIdx(0),
PointIdx(0),
PointIdx(1),
PointIdx(2),
PointIdx(2),
PointIdx(3),
],
end: [
PointIdx(1),
PointIdx(2),
PointIdx(2),
PointIdx(4),
PointIdx(3),
PointIdx(4),
],
),
point_neighbors: HalfOutputSegVec(
start: [
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: false,
),
),
],
end: [
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(5),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(5),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(5),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(5),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: false,
),
),
],
),
deleted: OutputSegVec(
inner: [
false,
false,
false,
false,
false,
false,
],
),
scan_west: OutputSegVec(
inner: [
None,
Some(OutputSegIdx(0)),
None,
None,
Some(OutputSegIdx(3)),
Some(OutputSegIdx(3)),
],
),
scan_east: OutputSegVec(
inner: [
None,
None,
None,
None,
None,
None,
],
),
scan_after: [],
orig_seg: OutputSegVec(
inner: [
SegIdx(0),
SegIdx(1),
SegIdx(3),
SegIdx(1),
SegIdx(3),
SegIdx(2),
],
),
positively_oriented: OutputSegVec(
inner: [
false,
true,
false,
true,
false,
false,
],
),
), Contours(
contours: [
Contour(
path: BezPath([
MoveTo(Point(
x: -1.0,
y: 0.0,
)),
CurveTo(Point(
x: -1.0,
y: -0.5,
), Point(
x: -0.5,
y: -0.25,
), Point(
x: 0.0,
y: 0.0,
)),
LineTo(Point(
x: -1.0,
y: 0.0,
)),
ClosePath,
]),
parent: None,
outer: true,
),
Contour(
path: BezPath([
MoveTo(Point(
x: 0.00000000000000016653345369377348,
y: 0.0,
)),
LineTo(Point(
x: 1.0,
y: 0.0,
)),
CurveTo(Point(
x: 1.0,
y: 0.5,
), Point(
x: 0.5,
y: 0.25,
), Point(
x: 0.0,
y: 0.0,
)),
ClosePath,
]),
parent: None,
outer: true,
),
],
))
@@ -0,0 +1,501 @@
---
source: src/topology.rs
expression: "(&top, contours, top.build_scan_line_orders())"
---
(Topology(
eps: 0.01,
correctness: GoodEnough,
tag: [
A,
A,
A,
A,
B,
B,
B,
B,
],
open_segs: [
[],
[],
[],
[],
[],
[],
[],
[],
],
winding: OutputSegVec(
inner: [
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 1,
),
clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 1,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 1,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 1,
),
clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
),
],
),
points: PointVec(
inner: [
Point(
y: -2.0,
x: -2.0,
),
Point(
y: -2.0,
x: 2.0,
),
Point(
y: -1.0,
x: -1.0,
),
Point(
y: -1.0,
x: 1.0,
),
Point(
y: 1.0,
x: -1.0,
),
Point(
y: 1.0,
x: 1.0,
),
Point(
y: 2.0,
x: -2.0,
),
Point(
y: 2.0,
x: 2.0,
),
],
),
point_idx: HalfOutputSegVec(
start: [
PointIdx(0),
PointIdx(0),
PointIdx(1),
PointIdx(2),
PointIdx(2),
PointIdx(3),
PointIdx(4),
PointIdx(6),
],
end: [
PointIdx(6),
PointIdx(1),
PointIdx(7),
PointIdx(4),
PointIdx(3),
PointIdx(5),
PointIdx(5),
PointIdx(7),
],
),
point_neighbors: HalfOutputSegVec(
start: [
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: false,
),
),
],
end: [
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(7),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(7),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(7),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(7),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(6),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(6),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(5),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(5),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(6),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(6),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(5),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(5),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: false,
),
),
],
),
deleted: OutputSegVec(
inner: [
false,
false,
false,
false,
false,
false,
false,
false,
],
),
scan_west: OutputSegVec(
inner: [
None,
Some(OutputSegIdx(0)),
Some(OutputSegIdx(0)),
Some(OutputSegIdx(0)),
Some(OutputSegIdx(3)),
Some(OutputSegIdx(3)),
Some(OutputSegIdx(0)),
None,
],
),
scan_east: OutputSegVec(
inner: [
None,
None,
None,
None,
None,
Some(OutputSegIdx(2)),
None,
None,
],
),
scan_after: [
(1.0, Some(OutputSegIdx(0)), Some(OutputSegIdx(2))),
],
orig_seg: OutputSegVec(
inner: [
SegIdx(3),
SegIdx(0),
SegIdx(1),
SegIdx(7),
SegIdx(4),
SegIdx(5),
SegIdx(6),
SegIdx(2),
],
),
positively_oriented: OutputSegVec(
inner: [
false,
true,
true,
false,
true,
true,
false,
false,
],
),
), Contours(
contours: [
Contour(
path: BezPath([
MoveTo(Point(
x: -2.0,
y: 2.0,
)),
LineTo(Point(
x: -2.0,
y: -2.0,
)),
LineTo(Point(
x: 2.0,
y: -2.0,
)),
LineTo(Point(
x: 2.0,
y: 2.0,
)),
LineTo(Point(
x: -2.0,
y: 2.0,
)),
ClosePath,
]),
parent: None,
outer: true,
),
Contour(
path: BezPath([
MoveTo(Point(
x: -1.0,
y: -1.0,
)),
LineTo(Point(
x: -1.0,
y: 1.0,
)),
LineTo(Point(
x: 1.0,
y: 1.0,
)),
LineTo(Point(
x: 1.0,
y: -1.0,
)),
LineTo(Point(
x: -1.0,
y: -1.0,
)),
ClosePath,
]),
parent: Some(ContourIdx(0)),
outer: false,
),
],
), ScanLineOrder(
west_map: HalfScanLineOrder(
inner: OutputSegVec(
inner: [
[],
[],
[
(-2.0, Some(OutputSegIdx(0))),
(-1.0, Some(OutputSegIdx(5))),
(1.0, Some(OutputSegIdx(0))),
],
[
(-1.0, Some(OutputSegIdx(0))),
],
[],
[
(-1.0, Some(OutputSegIdx(3))),
],
[],
[],
],
),
),
east_map: HalfScanLineOrder(
inner: OutputSegVec(
inner: [
[
(-2.0, Some(OutputSegIdx(2))),
(-1.0, Some(OutputSegIdx(3))),
(1.0, Some(OutputSegIdx(2))),
],
[],
[],
[
(-1.0, Some(OutputSegIdx(5))),
],
[],
[
(-1.0, Some(OutputSegIdx(2))),
],
[],
[],
],
),
),
))
@@ -0,0 +1,225 @@
---
source: src/topology.rs
expression: top
---
Topology(
eps: 0.01,
correctness: GoodEnough,
tag: [
A,
A,
A,
A,
],
open_segs: [
[],
[],
[],
[],
],
winding: OutputSegVec(
inner: [
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
),
],
),
points: PointVec(
inner: [
Point(
y: 0.0,
x: 0.0,
),
Point(
y: 0.0,
x: 1.0,
),
Point(
y: 1.0,
x: 0.0,
),
Point(
y: 1.0,
x: 1.0,
),
],
),
point_idx: HalfOutputSegVec(
start: [
PointIdx(0),
PointIdx(0),
PointIdx(1),
PointIdx(2),
],
end: [
PointIdx(2),
PointIdx(1),
PointIdx(3),
PointIdx(3),
],
),
point_neighbors: HalfOutputSegVec(
start: [
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: false,
),
),
],
end: [
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: false,
),
),
],
),
deleted: OutputSegVec(
inner: [
false,
false,
false,
false,
],
),
scan_west: OutputSegVec(
inner: [
None,
Some(OutputSegIdx(0)),
Some(OutputSegIdx(0)),
None,
],
),
scan_east: OutputSegVec(
inner: [
None,
None,
None,
None,
],
),
scan_after: [],
orig_seg: OutputSegVec(
inner: [
SegIdx(3),
SegIdx(0),
SegIdx(1),
SegIdx(2),
],
),
positively_oriented: OutputSegVec(
inner: [
false,
true,
true,
false,
],
),
)
@@ -0,0 +1,508 @@
---
source: src/topology.rs
expression: top
---
Topology(
eps: 0.01,
correctness: GoodEnough,
tag: [
A,
A,
A,
A,
B,
B,
B,
B,
],
open_segs: [
[],
[],
[],
[],
[],
[],
[],
[],
],
winding: OutputSegVec(
inner: [
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 1,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 1,
),
clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 1,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 1,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 1,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 1,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 1,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 1,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 1,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 1,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 1,
),
),
],
),
points: PointVec(
inner: [
Point(
y: 0.0,
x: 0.0,
),
Point(
y: 0.0,
x: 1.0,
),
Point(
y: 0.005,
x: 0.0,
),
Point(
y: 0.005,
x: 0.005,
),
Point(
y: 1.0,
x: -1.0,
),
Point(
y: 1.0,
x: 0.0,
),
Point(
y: 1.0,
x: 1.0,
),
Point(
y: 2.0,
x: 0.0,
),
],
),
point_idx: HalfOutputSegVec(
start: [
PointIdx(0),
PointIdx(0),
PointIdx(0),
PointIdx(0),
PointIdx(1),
PointIdx(2),
PointIdx(2),
PointIdx(3),
PointIdx(4),
PointIdx(5),
PointIdx(6),
],
end: [
PointIdx(2),
PointIdx(2),
PointIdx(3),
PointIdx(1),
PointIdx(6),
PointIdx(4),
PointIdx(5),
PointIdx(6),
PointIdx(7),
PointIdx(6),
PointIdx(7),
],
),
point_neighbors: HalfOutputSegVec(
start: [
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(6),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(5),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(5),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(5),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(6),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(6),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(9),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: false,
),
),
],
end: [
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(6),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(5),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(6),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(7),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(7),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(10),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(7),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(8),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(8),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(9),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(9),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(9),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(10),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(10),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(7),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(10),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(8),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(8),
first_half: false,
),
),
],
),
deleted: OutputSegVec(
inner: [
false,
true,
false,
false,
false,
false,
false,
false,
false,
false,
false,
],
),
scan_west: OutputSegVec(
inner: [
None,
Some(OutputSegIdx(0)),
Some(OutputSegIdx(1)),
Some(OutputSegIdx(2)),
Some(OutputSegIdx(2)),
None,
Some(OutputSegIdx(5)),
Some(OutputSegIdx(6)),
None,
Some(OutputSegIdx(8)),
Some(OutputSegIdx(8)),
],
),
scan_east: OutputSegVec(
inner: [
None,
None,
None,
None,
None,
None,
None,
Some(OutputSegIdx(4)),
None,
None,
None,
],
),
scan_after: [],
orig_seg: OutputSegVec(
inner: [
SegIdx(3),
SegIdx(7),
SegIdx(4),
SegIdx(0),
SegIdx(1),
SegIdx(7),
SegIdx(3),
SegIdx(4),
SegIdx(6),
SegIdx(2),
SegIdx(5),
],
),
positively_oriented: OutputSegVec(
inner: [
false,
false,
true,
true,
true,
false,
false,
true,
false,
false,
true,
],
),
)
@@ -0,0 +1,352 @@
---
source: src/topology.rs
expression: top
---
Topology(
eps: 0.01,
correctness: GoodEnough,
tag: [
A,
A,
A,
A,
A,
A,
A,
],
open_segs: [
[],
[],
[],
[],
[],
[],
[],
],
winding: OutputSegVec(
inner: [
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 2,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
),
],
),
points: PointVec(
inner: [
Point(
y: 0.0,
x: 0.0,
),
Point(
y: 0.0,
x: 0.5,
),
Point(
y: 0.0,
x: 1.0,
),
Point(
y: 0.5,
x: 0.5,
),
Point(
y: 1.0,
x: 0.0,
),
Point(
y: 1.0,
x: 1.0,
),
],
),
point_idx: HalfOutputSegVec(
start: [
PointIdx(0),
PointIdx(0),
PointIdx(1),
PointIdx(1),
PointIdx(1),
PointIdx(2),
PointIdx(4),
],
end: [
PointIdx(4),
PointIdx(1),
PointIdx(3),
PointIdx(3),
PointIdx(2),
PointIdx(5),
PointIdx(5),
],
),
point_neighbors: HalfOutputSegVec(
start: [
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: false,
),
),
],
end: [
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(6),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(6),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(5),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(5),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(6),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(6),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(5),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(5),
first_half: false,
),
),
],
),
deleted: OutputSegVec(
inner: [
false,
false,
true,
true,
false,
false,
false,
],
),
scan_west: OutputSegVec(
inner: [
None,
Some(OutputSegIdx(0)),
Some(OutputSegIdx(0)),
Some(OutputSegIdx(2)),
Some(OutputSegIdx(3)),
Some(OutputSegIdx(3)),
None,
],
),
scan_east: OutputSegVec(
inner: [
None,
None,
None,
None,
None,
None,
None,
],
),
scan_after: [
(0.5, Some(OutputSegIdx(0)), Some(OutputSegIdx(5))),
],
orig_seg: OutputSegVec(
inner: [
SegIdx(6),
SegIdx(0),
SegIdx(2),
SegIdx(1),
SegIdx(3),
SegIdx(4),
SegIdx(5),
],
),
positively_oriented: OutputSegVec(
inner: [
false,
true,
false,
true,
true,
true,
false,
],
),
)
@@ -0,0 +1,931 @@
---
source: src/topology.rs
expression: "(&top, contours, top.build_scan_line_orders())"
---
(Topology(
eps: 0.01,
correctness: GoodEnough,
tag: [
A,
A,
A,
A,
B,
B,
B,
B,
B,
B,
B,
B,
B,
B,
B,
B,
],
open_segs: [
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
],
winding: OutputSegVec(
inner: [
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 1,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 1,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 1,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: -1,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: -1,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: -1,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: -1,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 1,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 1,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 1,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 1,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 1,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
),
HalfSegmentWindingNumbers(
counter_clockwise: BinaryWindingNumber(
shape_a: 1,
shape_b: 0,
),
clockwise: BinaryWindingNumber(
shape_a: 0,
shape_b: 0,
),
),
],
),
points: PointVec(
inner: [
Point(
y: -2.0,
x: -2.0,
),
Point(
y: -2.0,
x: 2.0,
),
Point(
y: -1.0,
x: -4.0,
),
Point(
y: -1.0,
x: -3.0,
),
Point(
y: -1.0,
x: 3.0,
),
Point(
y: -1.0,
x: 4.0,
),
Point(
y: -0.5,
x: 3.0,
),
Point(
y: -0.5,
x: 4.0,
),
Point(
y: 0.5,
x: 3.0,
),
Point(
y: 0.5,
x: 4.0,
),
Point(
y: 1.0,
x: -4.0,
),
Point(
y: 1.0,
x: -3.0,
),
Point(
y: 1.0,
x: 3.0,
),
Point(
y: 1.0,
x: 4.0,
),
Point(
y: 2.0,
x: -2.0,
),
Point(
y: 2.0,
x: 2.0,
),
],
),
point_idx: HalfOutputSegVec(
start: [
PointIdx(0),
PointIdx(0),
PointIdx(1),
PointIdx(2),
PointIdx(2),
PointIdx(3),
PointIdx(4),
PointIdx(4),
PointIdx(5),
PointIdx(6),
PointIdx(8),
PointIdx(8),
PointIdx(9),
PointIdx(10),
PointIdx(12),
PointIdx(14),
],
end: [
PointIdx(14),
PointIdx(1),
PointIdx(15),
PointIdx(10),
PointIdx(3),
PointIdx(11),
PointIdx(6),
PointIdx(5),
PointIdx(7),
PointIdx(7),
PointIdx(12),
PointIdx(9),
PointIdx(13),
PointIdx(11),
PointIdx(13),
PointIdx(15),
],
),
point_neighbors: HalfOutputSegVec(
start: [
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(1),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(4),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(7),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(7),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(6),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(6),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(7),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(7),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(6),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(6),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(11),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(11),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(10),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(10),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(11),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(11),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(3),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(10),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(10),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(0),
first_half: false,
),
),
],
end: [
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(15),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(15),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(15),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(15),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(13),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(13),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(5),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(5),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(13),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(13),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(9),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(9),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(8),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(8),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(9),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(9),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(8),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(8),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(14),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(14),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(12),
first_half: true,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(12),
first_half: true,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(14),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(14),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(5),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(5),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(12),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(12),
first_half: false,
),
),
PointNeighbors(
clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: false,
),
counter_clockwise: HalfOutputSegIdx(
idx: OutputSegIdx(2),
first_half: false,
),
),
],
),
deleted: OutputSegVec(
inner: [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
],
),
scan_west: OutputSegVec(
inner: [
None,
Some(OutputSegIdx(0)),
Some(OutputSegIdx(0)),
None,
Some(OutputSegIdx(3)),
Some(OutputSegIdx(3)),
Some(OutputSegIdx(2)),
Some(OutputSegIdx(6)),
Some(OutputSegIdx(6)),
Some(OutputSegIdx(2)),
Some(OutputSegIdx(2)),
Some(OutputSegIdx(10)),
Some(OutputSegIdx(10)),
None,
Some(OutputSegIdx(2)),
None,
],
),
scan_east: OutputSegVec(
inner: [
None,
None,
None,
None,
None,
Some(OutputSegIdx(0)),
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
],
),
scan_after: [
(-0.5, Some(OutputSegIdx(2)), None),
(1.0, None, Some(OutputSegIdx(0))),
(1.0, Some(OutputSegIdx(2)), None),
],
orig_seg: OutputSegVec(
inner: [
SegIdx(3),
SegIdx(0),
SegIdx(1),
SegIdx(7),
SegIdx(4),
SegIdx(5),
SegIdx(9),
SegIdx(8),
SegIdx(11),
SegIdx(10),
SegIdx(13),
SegIdx(14),
SegIdx(15),
SegIdx(6),
SegIdx(12),
SegIdx(2),
],
),
positively_oriented: OutputSegVec(
inner: [
false,
true,
true,
false,
true,
true,
true,
false,
false,
true,
false,
true,
true,
false,
false,
false,
],
),
), Contours(
contours: [
Contour(
path: BezPath([
MoveTo(Point(
x: -2.0,
y: 2.0,
)),
LineTo(Point(
x: -2.0,
y: -2.0,
)),
LineTo(Point(
x: 2.0,
y: -2.0,
)),
LineTo(Point(
x: 2.0,
y: 2.0,
)),
LineTo(Point(
x: -2.0,
y: 2.0,
)),
ClosePath,
]),
parent: None,
outer: true,
),
Contour(
path: BezPath([
MoveTo(Point(
x: -4.0,
y: 1.0,
)),
LineTo(Point(
x: -4.0,
y: -1.0,
)),
LineTo(Point(
x: -3.0,
y: -1.0,
)),
LineTo(Point(
x: -3.0,
y: 1.0,
)),
LineTo(Point(
x: -4.0,
y: 1.0,
)),
ClosePath,
]),
parent: None,
outer: true,
),
Contour(
path: BezPath([
MoveTo(Point(
x: 3.0,
y: -0.5,
)),
LineTo(Point(
x: 3.0,
y: -1.0,
)),
LineTo(Point(
x: 4.0,
y: -1.0,
)),
LineTo(Point(
x: 4.0,
y: -0.5,
)),
LineTo(Point(
x: 3.0,
y: -0.5,
)),
ClosePath,
]),
parent: None,
outer: true,
),
Contour(
path: BezPath([
MoveTo(Point(
x: 3.0,
y: 1.0,
)),
LineTo(Point(
x: 3.0,
y: 0.5,
)),
LineTo(Point(
x: 4.0,
y: 0.5,
)),
LineTo(Point(
x: 4.0,
y: 1.0,
)),
LineTo(Point(
x: 3.0,
y: 1.0,
)),
ClosePath,
]),
parent: None,
outer: true,
),
],
), ScanLineOrder(
west_map: HalfScanLineOrder(
inner: OutputSegVec(
inner: [
[
(-1.0, Some(OutputSegIdx(5))),
(1.0, None),
],
[],
[
(-2.0, Some(OutputSegIdx(0))),
],
[],
[],
[
(-1.0, Some(OutputSegIdx(3))),
],
[
(-1.0, Some(OutputSegIdx(2))),
],
[],
[
(-1.0, Some(OutputSegIdx(6))),
],
[],
[
(0.5, Some(OutputSegIdx(2))),
],
[],
[
(0.5, Some(OutputSegIdx(10))),
],
[],
[],
[],
],
),
),
east_map: HalfScanLineOrder(
inner: OutputSegVec(
inner: [
[
(-2.0, Some(OutputSegIdx(2))),
],
[],
[
(-1.0, Some(OutputSegIdx(6))),
(-0.5, None),
(0.5, Some(OutputSegIdx(10))),
(1.0, None),
],
[
(-1.0, Some(OutputSegIdx(5))),
],
[],
[
(-1.0, Some(OutputSegIdx(0))),
],
[
(-1.0, Some(OutputSegIdx(8))),
],
[],
[],
[],
[
(0.5, Some(OutputSegIdx(12))),
],
[],
[],
[],
[],
[],
],
),
),
))
+33
View File
@@ -0,0 +1,33 @@
//! The sweep-line implementation.
//!
//! The details of this implementation are described in the `docs` directory.
//! The main entry point is [`Sweeper`], which computes all the intersection
//! points between a collection of line segments, and makes them available
//! sweep-line by sweep-line.
mod output_event;
mod range;
mod sweep_line;
pub use output_event::OutputEvent;
pub use range::{SegmentsConnectedAtX, SweepLineRange, SweepLineRangeBuffers};
pub use sweep_line::{ChangedInterval, Correctness, SweepLine, SweepLineBuffers, Sweeper};
use crate::Segments;
/// Runs the sweep-line algorithm, calling the provided callback on every output point.
pub fn sweep<C: FnMut(f64, OutputEvent)>(segments: &Segments, eps: f64, mut callback: C) {
let mut state = Sweeper::new(segments, eps);
let mut range_bufs = SweepLineRangeBuffers::default();
let mut line_bufs = SweepLineBuffers::default();
while let Some(mut line) = state.next_line(&mut line_bufs) {
let y = line.y();
while let Some(mut range) = line.next_range(&mut range_bufs, segments) {
while let Some(events) = range.events() {
for ev in events {
callback(y, ev);
}
}
}
}
}
+105
View File
@@ -0,0 +1,105 @@
use crate::{SegIdx, num::CheapOrderedFloat};
/// Describes the interaction between a line segment and a sweep-line.
///
/// In exact math, a non-horizontal line segment can interact with a sweep-line
/// in exactly one way: by intersecting it at a point. When dealing with inexact
/// math, intersections and re-orderings between line segments might force
/// our sweep-line algorithm to perturb the line segment. In that case, even
/// a non-horizontal line segment might enter and leave the sweep-line at two
/// different points.
///
/// `OutputEvent` is ordered by the smallest horizontal coordinate where
/// it intersects the sweep-line (i.e. [`OutputEvent::smaller_x`]).
///
/// The two points, `x0` and `x1`, are in sweep-line order. This doesn't
/// necessarily mean that `x0` is smaller than `x1`! Instead, it means that
/// when traversing the segment in sweep-line order (i.e. in increasing `y`,
/// and increasing `x` if the segment is horizontal) then it visits `x0`
/// before `x1`.
#[derive(Clone, Debug, PartialEq)]
pub struct OutputEvent {
/// The first horizontal coordinate on this sweep-line that we'd hit if
/// we were traversing the segment in sweep-line orientation.
pub x0: f64,
/// Does this line segment extend "above" (i.e. smaller `y`) this sweep-line?
///
/// If so, it will extend up from `x0`, because that's what the "sweep-line order"
/// means.
pub connected_above: bool,
/// The last horizontal coordinate on this sweep-line that we'd hit if
/// we were traversing the segment in sweep-line orientation.
pub x1: f64,
/// Does this line segment extend "below" (i.e. larger `y`) this sweep-line?
///
/// If so, it will extend down from `x1`, because that's what the "sweep-line order"
/// means.
pub connected_below: bool,
/// The segment that's interacting with the sweep line.
pub seg_idx: SegIdx,
/// The segment's index in the new sweep line (`None` for horizontal segments).
pub sweep_idx: Option<usize>,
/// The segment's index in the old sweep line (`None` for horizontal segments).
pub old_sweep_idx: Option<usize>,
}
impl Eq for OutputEvent {}
impl OutputEvent {
/// The smallest `x` coordinate at which the line segment touches the sweep-line.
pub fn smaller_x(&self) -> f64 {
self.x0.min(self.x1)
}
/// The largest `x` coordinate at which the line segment touches the sweep-line.
pub fn larger_x(&self) -> f64 {
self.x0.max(self.x1)
}
pub(crate) fn new(
seg_idx: SegIdx,
x0: f64,
connected_above: bool,
x1: f64,
connected_below: bool,
sweep_idx: Option<usize>,
old_sweep_idx: Option<usize>,
) -> Self {
Self {
x0,
connected_above,
x1,
connected_below,
seg_idx,
sweep_idx,
old_sweep_idx,
}
}
/// Does the line segment extend up from the horizontal coordinate `x`?
pub fn connected_above_at(&self, x: f64) -> bool {
x == self.x0 && self.connected_above
}
/// Does the line segment extend down from the horizontal coordinate `x`?
pub fn connected_below_at(&self, x: f64) -> bool {
x == self.x1 && self.connected_below
}
}
impl Ord for OutputEvent {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
CheapOrderedFloat::from(self.smaller_x())
.cmp(&CheapOrderedFloat::from(other.smaller_x()))
.then_with(|| {
CheapOrderedFloat::from(self.larger_x())
.cmp(&CheapOrderedFloat::from(other.larger_x()))
})
}
}
impl PartialOrd for OutputEvent {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
+445
View File
@@ -0,0 +1,445 @@
use crate::{SegIdx, num::CheapOrderedFloat};
use super::{ChangedInterval, OutputEvent, SweepLine};
/// A horizontal fragment.
///
/// Represents a horizontal part of a segment, which could be either an actual
/// horizontal segment or a little horizontal connector of a segment in a
/// sweep-line.
#[derive(Clone, Debug, PartialEq)]
struct HFrag {
/// The segment this horizontal fragment is a part of.
pub seg: SegIdx,
/// The first (smallest) horizontal position of this fragment.
pub start: f64,
/// The last (largest) horizontal position of this fragment.
pub end: f64,
/// Does this segment continue out of the sweep-line at `start`?
///
/// For a horizontal segment, this will always be false. On its own,
/// this doesn't tell you whether the segment points up or down at
/// `start`; for that, see `enter_first`.
pub connected_at_start: bool,
/// Does this segment continue out of the sweep-line at `end`?
///
/// For a horizontal segment, this will always be false.
pub connected_at_end: bool,
/// When traversing the segment in sweep-line order, does it visit
/// `start` first?
///
/// For example:
///
/// ```text
/// s_1 s_2 s_3
/// ╲ ╱ ╲
/// ╲ ╱ ╲
/// ─ ─ ─
/// ╲ ╱ ╲
/// ╲ ╱ ╲
/// ```
///
/// When moving from top to bottom (i.e. sweep-line order), s_1 and s_2
/// visit the larger horizontal position of the fragment before the smaller
/// one, so `enter_first` is false. On the other hand, s_3 has `enter_first`
/// as true.
pub enter_first: bool,
/// The position of the segment in the current sweep-line.
///
/// This will be `None` if, and only if, the segment is horizontal.
pub sweep_idx: Option<usize>,
/// The position of the segment in the old sweep-line.
///
/// This will be `None` if, and only if, the segment is horizontal.
pub old_sweep_idx: Option<usize>,
}
impl HFrag {
/// Given a segment's interaction with the sweep-line, returns the corresponding
/// horizontal fragment if there is one.
pub fn from_position(pos: OutputEvent) -> Option<Self> {
let OutputEvent {
x0,
connected_above,
x1,
connected_below,
..
} = pos;
if x0 == x1 {
return None;
}
let enter_first = x0 < x1;
let (start, end, connected_at_start, connected_at_end) = if enter_first {
(x0, x1, connected_above, connected_below)
} else {
(x1, x0, connected_below, connected_above)
};
Some(HFrag {
end,
start,
enter_first,
seg: pos.seg_idx,
connected_at_start,
connected_at_end,
sweep_idx: pos.sweep_idx,
old_sweep_idx: pos.old_sweep_idx,
})
}
/// Does this horizontal fragment's segment stick up from the sweep-line at `x`?
pub fn connected_above_at(&self, x: f64) -> bool {
(x == self.start && self.enter_first && self.connected_at_start)
|| (x == self.end && !self.enter_first && self.connected_at_end)
}
/// Does this horizontal fragment's segment stick down from the sweep-line at `x`?
pub fn connected_below_at(&self, x: f64) -> bool {
(x == self.start && !self.enter_first && self.connected_at_start)
|| (x == self.end && self.enter_first && self.connected_at_end)
}
}
/// Emits output events for a single sub-range of a single sweep-line.
///
/// This is constructed using [`SweepLine::next_range`]. By repeatedly
/// calling `SweepLineRange::increase_x` you can iterate over all
/// interesting horizontal positions, left to right (i.e. smaller `x` to larger
/// `x`).
#[derive(Debug)]
pub struct SweepLineRange<'bufs, 'state, 'segs> {
last_x: Option<f64>,
line: &'state SweepLine<'state, 'state, 'segs>,
bufs: &'bufs mut SweepLineRangeBuffers,
changed_interval: ChangedInterval,
output_events: &'state [OutputEvent],
}
impl<'bufs, 'state, 'segs> SweepLineRange<'bufs, 'state, 'segs> {
pub(crate) fn new(
line: &'state SweepLine<'state, 'state, 'segs>,
output_events: &'state [OutputEvent],
bufs: &'bufs mut SweepLineRangeBuffers,
changed_interval: ChangedInterval,
) -> Self {
let ret = Self {
last_x: None,
output_events,
changed_interval,
line,
bufs,
};
#[cfg(feature = "slow-asserts")]
ret.check_invariants();
ret
}
fn output_events(&self) -> &[OutputEvent] {
self.output_events
}
/// The current horizontal position, or `None` if we're finished.
pub fn x(&self) -> Option<f64> {
match (
self.bufs.active_horizontals.first(),
self.output_events().first(),
) {
(None, None) => None,
(None, Some(pos)) => Some(pos.smaller_x()),
(Some(h), None) => Some(h.end),
(Some(h), Some(pos)) => Some((h.end).min(pos.smaller_x())),
}
}
fn positions_at_x<'c, 'b: 'c>(&'b self, x: f64) -> impl Iterator<Item = &'b OutputEvent> + 'c {
self.output_events()
.iter()
.take_while(move |p| p.smaller_x() == x)
}
/// Updates a [`SegmentsConnectedAtX`] to reflect the current horizontal position.
pub fn update_segments_at_x(&self, segs: &mut SegmentsConnectedAtX) {
segs.connected_up.clear();
segs.connected_down.clear();
let Some(x) = self.x() else {
return;
};
for hseg in &self.bufs.active_horizontals {
if hseg.connected_above_at(x) {
segs.connected_up
.push((hseg.seg, hseg.old_sweep_idx.unwrap()));
}
if hseg.connected_below_at(x) {
segs.connected_down
.push((hseg.seg, hseg.sweep_idx.unwrap()));
}
}
for pos in self.positions_at_x(x) {
if pos.connected_above_at(x) {
segs.connected_up
.push((pos.seg_idx, pos.old_sweep_idx.unwrap()));
}
if pos.connected_below_at(x) {
segs.connected_down
.push((pos.seg_idx, pos.sweep_idx.unwrap()));
}
}
segs.connected_up
.sort_by_key(|(_seg_idx, sweep_idx)| *sweep_idx);
segs.connected_down
.sort_by_key(|(_seg_idx, sweep_idx)| *sweep_idx);
}
/// Iterates over the horizontal segments that are active at the current position.
///
/// This includes the segments that end here, but does not include the ones
/// that start here.
pub fn active_horizontals(&self) -> impl Iterator<Item = SegIdx> + '_ {
self.bufs.active_horizontals.iter().map(|hseg| hseg.seg)
}
/// Iterates over the horizontal segments that are active at the current position, along
/// with a boolean telling you whether the horizontal segment has the same orientation
/// as the segment that it belongs to.
///
/// This includes the segments that end here, but does not include the ones
/// that start here.
pub fn active_horizontals_and_orientations(&self) -> impl Iterator<Item = (SegIdx, bool)> + '_ {
self.bufs
.active_horizontals
.iter()
.map(|hseg| (hseg.seg, hseg.enter_first))
}
/// Returns the collection of all output events that end at the current
/// position, or `None` if this batcher is finished.
///
/// All the returned events start at the previous `x` position and end
/// at the current `x` position. In particular, if you alternate between
/// calling [`SweepLineRange::increase_x`] and this method, you'll
/// receive non-overlapping batches of output events.
pub fn events(&mut self) -> Option<Vec<OutputEvent>> {
let next_x = self.x()?;
let mut ret = Vec::new();
for h in &self.bufs.active_horizontals {
// unwrap: on the first event of this sweep line, active_horizontals is empty. So
// we only get here after last_x is populated.
let x0 = self.last_x.unwrap();
let x1 = next_x.min(h.end);
let connected_end = x1 == h.end && h.connected_at_end;
let connected_start = x0 == h.start && h.connected_at_start;
if h.enter_first {
ret.push(OutputEvent::new(
h.seg,
x0,
connected_start,
x1,
connected_end,
h.sweep_idx,
h.old_sweep_idx,
));
} else {
ret.push(OutputEvent::new(
h.seg,
x1,
connected_end,
x0,
connected_start,
h.sweep_idx,
h.old_sweep_idx,
));
}
}
// Drain the active horizontals, throwing away horizontal segments that end before
// the new x position.
self.drain_active_horizontals(next_x);
// Move along to the next horizontal position, processing the x events at the current
// position and either emitting them immediately or saving them as horizontals.
while let Some(ev) = self.output_events.first() {
if ev.smaller_x() > next_x {
break;
}
self.output_events = &self.output_events[1..];
if ev.x0 == ev.x1 {
// We push output event for points immediately.
ret.push(ev.clone());
} else if let Some(hseg) = HFrag::from_position(ev.clone()) {
// For horizontal segments, we don't output anything straight
// away. When we update the horizontal position and visit our
// horizontal segments, we'll output something.
self.bufs.active_horizontals.push(hseg);
}
}
self.bufs
.active_horizontals
.sort_by_key(|a| CheapOrderedFloat::from(a.end));
self.last_x = Some(next_x);
Some(ret)
}
/// Move along to the next horizontal position.
pub fn increase_x(&mut self) {
if let Some(x) = self.x() {
self.drain_active_horizontals(x);
while let Some(ev) = self.output_events.first() {
if ev.smaller_x() > x {
break;
}
self.output_events = &self.output_events[1..];
if let Some(hseg) = HFrag::from_position(ev.clone()) {
self.bufs.active_horizontals.push(hseg);
}
}
}
self.bufs
.active_horizontals
.sort_by_key(|a| CheapOrderedFloat::from(a.end));
}
fn drain_active_horizontals(&mut self, x: f64) {
let new_start = self
.bufs
.active_horizontals
.iter()
.position(|h| h.end > x)
.unwrap_or(self.bufs.active_horizontals.len());
self.bufs.active_horizontals.drain(..new_start);
}
/// The indices within the sweep line represented by this range.
pub fn seg_range(&self) -> ChangedInterval {
self.changed_interval.clone()
}
/// Returns an iterator over the segments in this range, ordered according
/// to the "old" sweep-line.
///
/// In addition to appearing in a different order, the set of segments returned
/// by this method and [`Self::segment_range`] may differ: segments that exit at the
/// current sweep line will be returned here and not there, while segments that
/// enter at the current sweep line will be returned there and not here.
pub fn old_segment_range(&self) -> impl Iterator<Item = SegIdx> + '_ {
let range = self.changed_interval.segs.clone();
self.line().old_segment_range(range)
}
/// Returns an iterator over the segments in this range, ordered according
/// to the "new" sweep-line.
///
/// In addition to appearing in a different order, the set of segments returned
/// by this method and [`Self::old_segment_range`] may differ: segments that exit at the
/// current sweep line will be returned there and not here, while segments that
/// enter at the current sweep line will be returned here and not there.
pub fn segment_range(&self) -> impl Iterator<Item = SegIdx> + '_ {
let range = self.changed_interval.segs.clone();
self.line().segment_range(range)
}
/// The sweep line that this is a range of.
pub fn line(&self) -> &SweepLine<'_, '_, 'segs> {
self.line
}
#[cfg(feature = "slow-asserts")]
fn check_invariants(&self) {
for ev in self.output_events() {
let seg = &self.line.segments()[ev.seg_idx];
let y = self.line.y();
let eps = self.line.eps();
// The curve comparison is guaranteed to give a strong
// ordering when the curves are 1.5 eps apart in ell_infty.
// That means the perturbed points should always be within
// 1.5 * sqrt(2) eps in ell_2, which is less than 2.5 eps. We
// compute distance to accuracy 0.5 eps, so we'll assert
// that our number is less than 3 eps.
// Ok, this doesn't work because of https://github.com/linebender/kurbo/issues/446
// For now, we'll use ell_infty comparisons.
//
// let n0 = seg.nearest(p0, eps / 2.0);
// let n1 = seg.nearest(p1, eps / 2.0);
// assert!(n0.distance_sq <= eps * eps * 9.0);
// assert!(n1.distance_sq <= eps * eps * 9.0);
let (lower, upper) = if seg.is_horizontal() {
(seg.start().x - 2.0 * eps, seg.end().x + 2.0 * eps)
} else {
(seg.lower(y, 2.0 * eps), seg.upper(y, 2.0 * eps))
};
assert!(ev.smaller_x() <= upper && lower <= ev.larger_x());
}
}
}
/// A re-usable struct for collecting segments at a single position on a sweep-line.
///
/// See [`SweepLineRange::update_segments_at_x`] for where this is used. At
/// any given time, this struct is implicitly associated to a single horizontal
/// position: the position of the `SweepLineRange` last time we were updated.
#[derive(Debug, Default)]
pub struct SegmentsConnectedAtX {
connected_up: Vec<(SegIdx, usize)>,
connected_down: Vec<(SegIdx, usize)>,
}
impl SegmentsConnectedAtX {
/// The segments that are connected up to a previous sweep-line at the
/// current horizontal position.
///
/// The returned iterator is sorted by the old sweep-line order. In other
/// words, it will return segments clockwise when viewed from the current
/// position.
pub fn connected_up(&self) -> impl Iterator<Item = SegIdx> + '_ {
self.connected_up.iter().map(|x| x.0)
}
/// The segments that are connected down to a subsequent sweep-line at the
/// current horizontal position.
///
/// The returned iterator is sorted by the new sweep-line order. In other
/// words, it will return segments counter-clockwise when viewed from the current
/// position.
pub fn connected_down(&self) -> impl Iterator<Item = SegIdx> + '_ {
self.connected_down.iter().map(|x| x.0)
}
}
/// Holds some buffers that are used when iterating over a sweep-line.
///
/// Save on re-allocation by allocating this once and reusing it in multiple calls to
/// [`SweepLine::next_range`].
#[derive(Clone, Debug, Default)]
pub struct SweepLineRangeBuffers {
/// All the horizontal segments overlapping with the sweep-line-range's current
/// horizontal position, ordered by ending position.
///
/// We could keep this in an ordered data structure, but it turns out to
/// be faster to just sort it regularly: every time we modify this, we also
/// advance the horizontal position and iterate over this entire collection.
/// Therefore, there's no asymptotic run-time to be gained by having a fast
/// way to insert/delete a single element.
active_horizontals: Vec<HFrag>,
}
impl SweepLineRangeBuffers {
pub(crate) fn clear(&mut self) {
self.active_horizontals.clear();
}
}
@@ -0,0 +1,136 @@
---
source: src/sweep/sweep_line.rs
expression: "snapshot_outputs(segs, 0.25)"
---
[
Output(
y: 0.0,
order: SegmentOrder(
segs: [
SegmentOrderEntry(
seg: SegIdx(0),
exit: false,
enter: true,
lower_bound: -1.25,
upper_bound: 0.8500000000000001,
in_changed_interval: true,
old_idx: Some(0),
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(1),
exit: false,
enter: true,
lower_bound: -0.25,
upper_bound: 0.25,
in_changed_interval: true,
old_idx: Some(1),
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(2),
exit: false,
enter: true,
lower_bound: 0.04999999999999999,
upper_bound: 0.55,
in_changed_interval: true,
old_idx: Some(2),
old_seg: None,
),
],
),
changed: [
ChangedInterval(
segs: Range(
start: 0,
end: 3,
),
horizontals: None,
),
],
),
Output(
y: 0.5,
order: SegmentOrder(
segs: [
SegmentOrderEntry(
seg: SegIdx(0),
exit: false,
enter: false,
lower_bound: -1.25,
upper_bound: 0.8500000000000001,
in_changed_interval: false,
old_idx: None,
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(1),
exit: false,
enter: false,
lower_bound: -0.25,
upper_bound: 0.25,
in_changed_interval: false,
old_idx: None,
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(2),
exit: false,
enter: false,
lower_bound: 0.04999999999999999,
upper_bound: 0.55,
in_changed_interval: false,
old_idx: None,
old_seg: None,
),
],
),
changed: [],
),
Output(
y: 1.0,
order: SegmentOrder(
segs: [
SegmentOrderEntry(
seg: SegIdx(0),
exit: true,
enter: false,
lower_bound: -1.25,
upper_bound: 0.8500000000000001,
in_changed_interval: true,
old_idx: Some(0),
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(1),
exit: true,
enter: false,
lower_bound: -0.25,
upper_bound: 0.25,
in_changed_interval: true,
old_idx: Some(1),
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(2),
exit: true,
enter: false,
lower_bound: 0.04999999999999999,
upper_bound: 0.55,
in_changed_interval: true,
old_idx: Some(2),
old_seg: None,
),
],
),
changed: [
ChangedInterval(
segs: Range(
start: 0,
end: 3,
),
horizontals: None,
),
],
),
]
@@ -0,0 +1,361 @@
---
source: src/sweep/sweep_line.rs
expression: "snapshot_outputs(segs, 0.1)"
---
[
Output(
y: -1.0,
order: SegmentOrder(
segs: [
SegmentOrderEntry(
seg: SegIdx(5),
exit: false,
enter: true,
lower_bound: -0.1,
upper_bound: 1.1,
in_changed_interval: true,
old_idx: Some(0),
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(4),
exit: false,
enter: true,
lower_bound: 0.9,
upper_bound: 1.1,
in_changed_interval: true,
old_idx: Some(1),
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(1),
exit: false,
enter: true,
lower_bound: 0.9,
upper_bound: 1.1,
in_changed_interval: true,
old_idx: Some(2),
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(2),
exit: false,
enter: true,
lower_bound: 0.9,
upper_bound: 2.1,
in_changed_interval: true,
old_idx: Some(3),
old_seg: None,
),
],
),
changed: [
ChangedInterval(
segs: Range(
start: 0,
end: 4,
),
horizontals: None,
),
],
),
Output(
y: -0.00000000000000005551115123125783,
order: SegmentOrder(
segs: [
SegmentOrderEntry(
seg: SegIdx(5),
exit: false,
enter: false,
lower_bound: -0.1,
upper_bound: 1.1,
in_changed_interval: false,
old_idx: None,
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(4),
exit: false,
enter: false,
lower_bound: 0.9,
upper_bound: 1.1,
in_changed_interval: false,
old_idx: None,
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(1),
exit: false,
enter: false,
lower_bound: 0.9,
upper_bound: 1.1,
in_changed_interval: false,
old_idx: None,
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(2),
exit: false,
enter: false,
lower_bound: 0.9,
upper_bound: 2.1,
in_changed_interval: false,
old_idx: None,
old_seg: None,
),
],
),
changed: [],
),
Output(
y: 0.0,
order: SegmentOrder(
segs: [
SegmentOrderEntry(
seg: SegIdx(0),
exit: true,
enter: true,
lower_bound: -0.1,
upper_bound: 1.1,
in_changed_interval: true,
old_idx: Some(0),
old_seg: Some(SegIdx(5)),
),
SegmentOrderEntry(
seg: SegIdx(4),
exit: false,
enter: false,
lower_bound: 0.9,
upper_bound: 1.1,
in_changed_interval: false,
old_idx: None,
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(1),
exit: false,
enter: false,
lower_bound: 0.9,
upper_bound: 1.1,
in_changed_interval: false,
old_idx: None,
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(3),
exit: true,
enter: true,
lower_bound: 0.9,
upper_bound: 2.1,
in_changed_interval: true,
old_idx: Some(3),
old_seg: Some(SegIdx(2)),
),
],
),
changed: [
ChangedInterval(
segs: Range(
start: 0,
end: 1,
),
horizontals: None,
),
ChangedInterval(
segs: Range(
start: 3,
end: 4,
),
horizontals: None,
),
],
),
Output(
y: 0.95,
order: SegmentOrder(
segs: [
SegmentOrderEntry(
seg: SegIdx(0),
exit: false,
enter: false,
lower_bound: -0.1,
upper_bound: 1.1,
in_changed_interval: false,
old_idx: None,
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(4),
exit: false,
enter: false,
lower_bound: 0.9,
upper_bound: 1.1,
in_changed_interval: false,
old_idx: None,
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(1),
exit: false,
enter: false,
lower_bound: 0.9,
upper_bound: 1.1,
in_changed_interval: false,
old_idx: None,
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(3),
exit: false,
enter: false,
lower_bound: 0.9,
upper_bound: 2.1,
in_changed_interval: false,
old_idx: None,
old_seg: None,
),
],
),
changed: [],
),
Output(
y: 0.9749999999999999,
order: SegmentOrder(
segs: [
SegmentOrderEntry(
seg: SegIdx(0),
exit: false,
enter: false,
lower_bound: -0.1,
upper_bound: 1.1,
in_changed_interval: false,
old_idx: None,
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(4),
exit: false,
enter: false,
lower_bound: 0.9,
upper_bound: 1.1,
in_changed_interval: false,
old_idx: None,
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(1),
exit: false,
enter: false,
lower_bound: 0.9,
upper_bound: 1.1,
in_changed_interval: false,
old_idx: None,
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(3),
exit: false,
enter: false,
lower_bound: 0.9,
upper_bound: 2.1,
in_changed_interval: false,
old_idx: None,
old_seg: None,
),
],
),
changed: [],
),
Output(
y: 0.9999999999999999,
order: SegmentOrder(
segs: [
SegmentOrderEntry(
seg: SegIdx(0),
exit: true,
enter: false,
lower_bound: -0.1,
upper_bound: 1.1,
in_changed_interval: true,
old_idx: Some(0),
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(4),
exit: false,
enter: false,
lower_bound: 0.9,
upper_bound: 1.1,
in_changed_interval: true,
old_idx: Some(1),
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(1),
exit: true,
enter: false,
lower_bound: 0.9,
upper_bound: 1.1,
in_changed_interval: true,
old_idx: Some(2),
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(3),
exit: false,
enter: false,
lower_bound: 0.9,
upper_bound: 2.1,
in_changed_interval: true,
old_idx: Some(3),
old_seg: None,
),
],
),
changed: [
ChangedInterval(
segs: Range(
start: 0,
end: 4,
),
horizontals: None,
),
],
),
Output(
y: 1.0,
order: SegmentOrder(
segs: [
SegmentOrderEntry(
seg: SegIdx(4),
exit: true,
enter: false,
lower_bound: 0.9,
upper_bound: 1.1,
in_changed_interval: true,
old_idx: Some(0),
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(3),
exit: true,
enter: false,
lower_bound: 0.9,
upper_bound: 2.1,
in_changed_interval: true,
old_idx: Some(1),
old_seg: None,
),
],
),
changed: [
ChangedInterval(
segs: Range(
start: 0,
end: 2,
),
horizontals: None,
),
],
),
]
@@ -0,0 +1,84 @@
---
source: src/sweep/sweep_line.rs
expression: "snapshot_outputs(segs, 0.01)"
---
[
Output(
y: 0.0,
order: SegmentOrder(
segs: [
SegmentOrderEntry(
seg: SegIdx(3),
exit: false,
enter: true,
lower_bound: -0.01,
upper_bound: 0.01,
in_changed_interval: true,
old_idx: Some(0),
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(1),
exit: false,
enter: true,
lower_bound: 0.99,
upper_bound: 1.01,
in_changed_interval: true,
old_idx: Some(1),
old_seg: None,
),
],
),
changed: [
ChangedInterval(
segs: Range(
start: 0,
end: 2,
),
horizontals: Some(Range(
start: 0,
end: 1,
)),
),
],
),
Output(
y: 1.0,
order: SegmentOrder(
segs: [
SegmentOrderEntry(
seg: SegIdx(3),
exit: true,
enter: false,
lower_bound: -0.01,
upper_bound: 0.01,
in_changed_interval: true,
old_idx: Some(0),
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(1),
exit: true,
enter: false,
lower_bound: 0.99,
upper_bound: 1.01,
in_changed_interval: true,
old_idx: Some(1),
old_seg: None,
),
],
),
changed: [
ChangedInterval(
segs: Range(
start: 0,
end: 2,
),
horizontals: Some(Range(
start: 0,
end: 1,
)),
),
],
),
]
@@ -0,0 +1,292 @@
---
source: src/sweep/sweep_line.rs
expression: "snapshot_outputs(segs, eps)"
---
[
Output(
y: 0.0,
order: SegmentOrder(
segs: [
SegmentOrderEntry(
seg: SegIdx(3),
exit: false,
enter: true,
lower_bound: -0.01,
upper_bound: 0.01,
in_changed_interval: true,
old_idx: Some(0),
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(7),
exit: false,
enter: true,
lower_bound: -1.01,
upper_bound: 0.01,
in_changed_interval: true,
old_idx: Some(1),
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(4),
exit: false,
enter: true,
lower_bound: -0.01,
upper_bound: 1.01,
in_changed_interval: true,
old_idx: Some(2),
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(1),
exit: false,
enter: true,
lower_bound: 0.99,
upper_bound: 1.01,
in_changed_interval: true,
old_idx: Some(3),
old_seg: None,
),
],
),
changed: [
ChangedInterval(
segs: Range(
start: 0,
end: 4,
),
horizontals: Some(Range(
start: 0,
end: 1,
)),
),
],
),
Output(
y: 0.005,
order: SegmentOrder(
segs: [
SegmentOrderEntry(
seg: SegIdx(7),
exit: false,
enter: false,
lower_bound: -1.01,
upper_bound: 0.01,
in_changed_interval: true,
old_idx: Some(1),
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(3),
exit: false,
enter: false,
lower_bound: -0.01,
upper_bound: 0.01,
in_changed_interval: true,
old_idx: Some(0),
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(4),
exit: false,
enter: false,
lower_bound: -0.01,
upper_bound: 1.01,
in_changed_interval: true,
old_idx: Some(2),
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(1),
exit: false,
enter: false,
lower_bound: 0.99,
upper_bound: 1.01,
in_changed_interval: false,
old_idx: None,
old_seg: None,
),
],
),
changed: [
ChangedInterval(
segs: Range(
start: 0,
end: 3,
),
horizontals: None,
),
],
),
Output(
y: 0.995,
order: SegmentOrder(
segs: [
SegmentOrderEntry(
seg: SegIdx(7),
exit: false,
enter: false,
lower_bound: -1.01,
upper_bound: 0.01,
in_changed_interval: false,
old_idx: None,
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(3),
exit: false,
enter: false,
lower_bound: -0.01,
upper_bound: 0.01,
in_changed_interval: false,
old_idx: None,
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(4),
exit: false,
enter: false,
lower_bound: -0.01,
upper_bound: 1.01,
in_changed_interval: false,
old_idx: None,
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(1),
exit: false,
enter: false,
lower_bound: 0.99,
upper_bound: 1.01,
in_changed_interval: false,
old_idx: None,
old_seg: None,
),
],
),
changed: [],
),
Output(
y: 1.0,
order: SegmentOrder(
segs: [
SegmentOrderEntry(
seg: SegIdx(6),
exit: true,
enter: true,
lower_bound: -1.01,
upper_bound: 0.01,
in_changed_interval: true,
old_idx: Some(0),
old_seg: Some(SegIdx(7)),
),
SegmentOrderEntry(
seg: SegIdx(3),
exit: true,
enter: false,
lower_bound: -0.01,
upper_bound: 0.01,
in_changed_interval: true,
old_idx: Some(1),
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(5),
exit: true,
enter: true,
lower_bound: -0.01,
upper_bound: 1.01,
in_changed_interval: true,
old_idx: Some(2),
old_seg: Some(SegIdx(4)),
),
SegmentOrderEntry(
seg: SegIdx(1),
exit: true,
enter: false,
lower_bound: 0.99,
upper_bound: 1.01,
in_changed_interval: true,
old_idx: Some(3),
old_seg: None,
),
],
),
changed: [
ChangedInterval(
segs: Range(
start: 0,
end: 4,
),
horizontals: Some(Range(
start: 0,
end: 1,
)),
),
],
),
Output(
y: 1.9975,
order: SegmentOrder(
segs: [
SegmentOrderEntry(
seg: SegIdx(6),
exit: false,
enter: false,
lower_bound: -1.01,
upper_bound: 0.01,
in_changed_interval: false,
old_idx: None,
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(5),
exit: false,
enter: false,
lower_bound: -0.01,
upper_bound: 1.01,
in_changed_interval: false,
old_idx: None,
old_seg: None,
),
],
),
changed: [],
),
Output(
y: 2.0,
order: SegmentOrder(
segs: [
SegmentOrderEntry(
seg: SegIdx(6),
exit: true,
enter: false,
lower_bound: -1.01,
upper_bound: 0.01,
in_changed_interval: true,
old_idx: Some(0),
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(5),
exit: true,
enter: false,
lower_bound: -0.01,
upper_bound: 1.01,
in_changed_interval: true,
old_idx: Some(1),
old_seg: None,
),
],
),
changed: [
ChangedInterval(
segs: Range(
start: 0,
end: 2,
),
horizontals: None,
),
],
),
]
@@ -0,0 +1,202 @@
---
source: src/sweep/sweep_line.rs
expression: "snapshot_outputs(segs, 0.1)"
---
[
Output(
y: -0.5,
order: SegmentOrder(
segs: [
SegmentOrderEntry(
seg: SegIdx(7),
exit: false,
enter: true,
lower_bound: -0.6,
upper_bound: -0.4,
in_changed_interval: true,
old_idx: Some(0),
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(5),
exit: false,
enter: true,
lower_bound: 0.4,
upper_bound: 0.6,
in_changed_interval: true,
old_idx: Some(1),
old_seg: None,
),
],
),
changed: [
ChangedInterval(
segs: Range(
start: 0,
end: 2,
),
horizontals: Some(Range(
start: 0,
end: 1,
)),
),
],
),
Output(
y: 0.0,
order: SegmentOrder(
segs: [
SegmentOrderEntry(
seg: SegIdx(7),
exit: false,
enter: false,
lower_bound: -0.6,
upper_bound: -0.4,
in_changed_interval: false,
old_idx: None,
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(3),
exit: false,
enter: true,
lower_bound: -0.1,
upper_bound: 0.1,
in_changed_interval: true,
old_idx: Some(1),
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(5),
exit: false,
enter: false,
lower_bound: 0.4,
upper_bound: 0.6,
in_changed_interval: false,
old_idx: Some(2),
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(1),
exit: false,
enter: true,
lower_bound: 0.9,
upper_bound: 1.1,
in_changed_interval: true,
old_idx: Some(3),
old_seg: None,
),
],
),
changed: [
ChangedInterval(
segs: Range(
start: 1,
end: 4,
),
horizontals: Some(Range(
start: 0,
end: 1,
)),
),
],
),
Output(
y: 0.5,
order: SegmentOrder(
segs: [
SegmentOrderEntry(
seg: SegIdx(7),
exit: true,
enter: false,
lower_bound: -0.6,
upper_bound: -0.4,
in_changed_interval: true,
old_idx: Some(0),
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(3),
exit: false,
enter: false,
lower_bound: -0.1,
upper_bound: 0.1,
in_changed_interval: false,
old_idx: Some(1),
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(5),
exit: true,
enter: false,
lower_bound: 0.4,
upper_bound: 0.6,
in_changed_interval: true,
old_idx: Some(2),
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(1),
exit: false,
enter: false,
lower_bound: 0.9,
upper_bound: 1.1,
in_changed_interval: false,
old_idx: None,
old_seg: None,
),
],
),
changed: [
ChangedInterval(
segs: Range(
start: 0,
end: 3,
),
horizontals: Some(Range(
start: 0,
end: 1,
)),
),
],
),
Output(
y: 1.0,
order: SegmentOrder(
segs: [
SegmentOrderEntry(
seg: SegIdx(3),
exit: true,
enter: false,
lower_bound: -0.1,
upper_bound: 0.1,
in_changed_interval: true,
old_idx: Some(0),
old_seg: None,
),
SegmentOrderEntry(
seg: SegIdx(1),
exit: true,
enter: false,
lower_bound: 0.9,
upper_bound: 1.1,
in_changed_interval: true,
old_idx: Some(1),
old_seg: None,
),
],
),
changed: [
ChangedInterval(
segs: Range(
start: 0,
end: 2,
),
horizontals: Some(Range(
start: 0,
end: 1,
)),
),
],
),
]
File diff suppressed because it is too large. Load diff
+2688
View File
File diff suppressed because it is too large. Load diff
+955
View File
@@ -0,0 +1,955 @@
use arrayvec::ArrayVec;
#[cfg(test)]
use serde::ser::SerializeSeq;
/// A container with fast random access and random insertion/deletion.
///
/// The implementation is based on a short, wide, more-or-less-balanced tree.
/// You get to choose how wide it is: `B` is the maximum size of each node,
/// and we ensure that every node but the root has at least `B/2` elements.
/// The depth of the tree is therefore of order `log_B (n)`, where `n`
/// is the number of elements.
#[derive(Clone, Debug)]
pub struct TreeVec<T, const B: usize> {
root: Box<Node<T, B>>,
}
#[cfg(test)]
impl<T: serde::Serialize, const B: usize> serde::Serialize for TreeVec<T, B> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut seq = serializer.serialize_seq(Some(self.len()))?;
for x in self.iter() {
seq.serialize_element(x)?;
}
seq.end()
}
}
/// A single node in the tree.
#[cfg_attr(test, derive(serde::Serialize))]
#[derive(Clone, Debug)]
enum Node<T, const B: usize> {
Leaf {
/// Guaranteed to have at least `B/2` elements (and in particular,
/// to be non-empty) *except* if this node happens to be the root
/// of the tree.
data: ArrayVec<T, B>,
},
/// An internal node. The two arrays, `size` and `children`, are guaranteed
/// to be non-empty. And unless this node is the root of the tree,
/// they will have at least `B/2` elements.
Internal {
/// For each child, the number of elements in its subtree.
///
/// (And so the number of elements in my subtree is the sum of these.)
size: ArrayVec<usize, B>,
/// The children. Each subtree is guaranteed to have the same height.
children: ArrayVec<Box<Node<T, B>>, B>,
},
}
/// The result of inserting an element into a subtree.
enum InsertResult<T, const B: usize> {
/// It fits!
Done,
/// It didn't fit, and the subtree had to be split into two subtrees.
/// The node in here is the second of the two subtrees.
Split(Box<Node<T, B>>),
}
/// The result of removing an element from a subtree.
enum RemoveResult<T> {
/// It's gone!
Done(T),
/// It's gone, but the root of the subtree now has too few children.
Undersize(T),
}
impl<T> RemoveResult<T> {
fn into_inner(self) -> T {
match self {
RemoveResult::Done(ret) | RemoveResult::Undersize(ret) => ret,
}
}
}
/// The result of fixing up the sizes of two sibling subtrees.
enum MergeResult {
/// The two subtrees both fit in a single node: one has been drained and added
/// to the other.
Absorbed,
/// Some elements were moved from one subtree to another, and now they both
/// have valid sizes.
Rebalanced,
}
/// Which subtree does the given offset belong to?
///
/// Returns the index of the subtree containing that offset, and the offset
/// relative to the subtree.
fn child_idx(sizes: &[usize], mut offset: usize) -> Option<(usize, usize)> {
for (idx, &size) in sizes.iter().enumerate() {
if size > offset {
return Some((idx, offset));
}
offset -= size;
}
None
}
impl<T, const B: usize> Node<T, B> {
/// Returns the size of the subtree rooted at this node.
fn subtree_size(&self) -> usize {
match self {
Node::Leaf { data } => data.len(),
Node::Internal { size, .. } => size.iter().copied().sum(),
}
}
/// Gets the item at offset `offset`, relative to the subtree rooted at this node.
fn get(&self, offset: usize) -> Option<&T> {
match self {
Node::Leaf { data } => data.get(offset),
Node::Internal { size, children } => {
let (idx, offset) = child_idx(size, offset)?;
children[idx].get(offset)
}
}
}
/// Gets (mutably) the item at offset `offset`, relative to the subtree rooted at this node.
fn get_mut(&mut self, offset: usize) -> Option<&mut T> {
match self {
Node::Leaf { data } => data.get_mut(offset),
Node::Internal { size, children } => {
let (idx, offset) = child_idx(size, offset)?;
children[idx].get_mut(offset)
}
}
}
/// Returns the first item in the subtree.
///
/// `tree.first()` is equivalent to `tree.get(0)`, but faster.
fn first(&self) -> Option<&T> {
match self {
Node::Leaf { data } => data.first(),
Node::Internal { children, .. } => children.first().and_then(|c| c.first()),
}
}
/// Inserts an element into the subtree rooted at this node.
///
/// The offset is relative to this subtree.
///
/// If this insertion causes the subtree to overflow, the root node will be split.
/// In this case, this node will be modified, and the new sibling will be returned.
fn insert(&mut self, offset: usize, element: T) -> InsertResult<T, B> {
match self {
Node::Leaf { data } => {
if data.is_full() {
let mut second_half: ArrayVec<T, B> = data.drain(B / 2..).collect();
if offset <= B / 2 {
data.insert(offset, element)
} else {
second_half.insert(offset - B / 2, element)
}
InsertResult::Split(Box::new(Node::Leaf { data: second_half }))
} else {
data.insert(offset, element);
InsertResult::Done
}
}
Node::Internal { size, children } => {
let (idx, offset) = if offset > 0 {
// unwrap: if this fails, it's out-of-bounds
let (idx, offset) = child_idx(size, offset - 1).unwrap();
(idx, offset + 1)
} else {
(0, 0)
};
match children[idx].insert(offset, element) {
InsertResult::Done => {
size[idx] += 1;
InsertResult::Done
}
InsertResult::Split(node) => {
size[idx] = children[idx].subtree_size();
if children.is_full() {
let mut second_half_children: ArrayVec<_, B> =
children.drain(B / 2..).collect();
let mut second_half_size: ArrayVec<_, B> =
size.drain(B / 2..).collect();
if idx < B / 2 {
size.insert(idx + 1, node.subtree_size());
children.insert(idx + 1, node);
} else {
second_half_size.insert(idx + 1 - B / 2, node.subtree_size());
second_half_children.insert(idx + 1 - B / 2, node);
}
InsertResult::Split(Box::new(Node::Internal {
size: second_half_size,
children: second_half_children,
}))
} else {
size.insert(idx + 1, node.subtree_size());
children.insert(idx + 1, node);
InsertResult::Done
}
}
}
}
}
}
/// We're undersized (by one; we only support removing a single element at a time)
/// and maybe our right_sibling can help by giving us some more elements.
fn merge_from_right(&mut self, right_sibling: &mut Node<T, B>) -> MergeResult {
match (self, right_sibling) {
(Node::Leaf { data: left_data }, Node::Leaf { data: right_data }) => {
debug_assert!(right_data.len() >= left_data.len());
if left_data.len() + right_data.len() <= B {
left_data.extend(right_data.drain(..));
MergeResult::Absorbed
} else {
// Since we're only undersized by one, we could just move a single
// element over. It's probably more efficient to rebalance more
// aggressively, so we can avoid rebalancing in the future. So
// here we try to equalize the sizes.
let count = (right_data.len() - left_data.len()) / 2;
// We're undersized, and since the two of us don't fit in a single
// node, right_data must have at least B/2 + 1 elements. Therefore
// the difference in sizes is at least 2.
debug_assert!(count > 0);
left_data.extend(right_data.drain(..count));
MergeResult::Rebalanced
}
}
(
Node::Internal {
size: left_size,
children: left_children,
},
Node::Internal {
size: right_size,
children: right_children,
},
) => {
if left_children.len() + right_children.len() <= B {
left_size.extend(right_size.drain(..));
left_children.extend(right_children.drain(..));
MergeResult::Absorbed
} else {
let count = (right_children.len() - left_children.len()) / 2;
debug_assert!(count > 0);
left_children.extend(right_children.drain(..count));
left_size.extend(right_size.drain(..count));
MergeResult::Rebalanced
}
}
_ => unreachable!(),
}
}
/// We're undersized (by one; we only support removing a single element at a time)
/// and maybe our right_sibling can help by giving us some more elements.
fn merge_from_left(&mut self, left_sibling: &mut Node<T, B>) -> MergeResult {
match (left_sibling, self) {
(Node::Leaf { data: left_data }, Node::Leaf { data: right_data }) => {
debug_assert!(right_data.len() <= left_data.len());
if left_data.len() + right_data.len() <= B {
// There's no efficient and safe way to insert a bunch of
// data at the beginning of right, so instead we append to
// left and then swap them.
left_data.extend(right_data.drain(..));
std::mem::swap(left_data, right_data);
MergeResult::Absorbed
} else {
// Unlike merge_from_right, here we only move a single element
// from the left to the right. This is just because safe rust
// makes it tricky to efficiently move more; ideally we'd also
// be rebalancing here.
right_data.insert(0, left_data.pop().unwrap());
MergeResult::Rebalanced
}
}
(
Node::Internal {
size: left_size,
children: left_children,
},
Node::Internal {
size: right_size,
children: right_children,
},
) => {
if left_children.len() + right_children.len() <= B {
left_size.extend(right_size.drain(..));
left_children.extend(right_children.drain(..));
std::mem::swap(left_children, right_children);
std::mem::swap(left_size, right_size);
MergeResult::Absorbed
} else {
right_children.insert(0, left_children.pop().unwrap());
right_size.insert(0, left_size.pop().unwrap());
MergeResult::Rebalanced
}
}
_ => unreachable!(),
}
}
/// Remove an element from a given offset in this subtree.
///
/// This may leave the root of the subtree undersized, in which case the
/// return value will let you know.
fn remove(&mut self, offset: usize) -> RemoveResult<T> {
match self {
Node::Leaf { data } => {
let ret = data.remove(offset);
if data.len() < B / 2 {
RemoveResult::Undersize(ret)
} else {
RemoveResult::Done(ret)
}
}
Node::Internal { size, children } => {
let (idx, offset) = child_idx(size, offset).unwrap();
size[idx] -= 1;
match children[idx].remove(offset) {
RemoveResult::Done(ret) => RemoveResult::Done(ret),
RemoveResult::Undersize(ret) => {
// We now have an undersized node at index i. We try
// to merge in more from the right neighbor (because
// that's the more efficient merge direction in our
// implementation). But if `i` is the last element, we
// fall back to merging from the left.
if idx + 1 < children.len() {
// This little incantation seems to be the easiest
// (stable, safe) way to get two &muts to different
// elements.
let (a, b) = children.split_at_mut(idx + 1);
let cur = a.last_mut().unwrap();
let next = b.first_mut().unwrap();
match cur.merge_from_right(next) {
MergeResult::Absorbed => {
size[idx] = cur.subtree_size();
children.remove(idx + 1);
size.remove(idx + 1);
if children.len() < B / 2 {
RemoveResult::Undersize(ret)
} else {
RemoveResult::Done(ret)
}
}
MergeResult::Rebalanced => {
size[idx] = cur.subtree_size();
size[idx + 1] = next.subtree_size();
RemoveResult::Done(ret)
}
}
} else {
// We couldn't merge from the right, since `idx`
// is at the end. Since internal nodes can't be
// undersized, as long as B >= 4 we must have at
// least 2 elements and so there is something to
// our left. (Maybe we should have something to
// ensure that B >= 4?)
debug_assert!(idx > 0);
let (a, b) = children.split_at_mut(idx);
let prev = a.last_mut().unwrap();
let cur = b.first_mut().unwrap();
match cur.merge_from_left(prev) {
MergeResult::Absorbed => {
size[idx] = cur.subtree_size();
children.remove(idx - 1);
size.remove(idx - 1);
if children.len() < B / 2 {
RemoveResult::Undersize(ret)
} else {
RemoveResult::Done(ret)
}
}
MergeResult::Rebalanced => {
size[idx - 1] = prev.subtree_size();
size[idx] = cur.subtree_size();
RemoveResult::Done(ret)
}
}
}
}
}
}
}
}
/// Returns the offset (in this subtree) of the first element for which `pred` returns false.
fn partition_point<P>(&self, mut pred: P) -> usize
where
P: FnMut(&T) -> bool,
{
match self {
Node::Leaf { data } => data.partition_point(pred),
Node::Internal { children, size } => {
// It seems to be faster if we first do a binary search over our subtrees,
// and then we descend into the interesting subtree.
let child_idx = children.partition_point(|child| pred(child.first().unwrap()));
if child_idx > 0 {
children[child_idx - 1].partition_point(pred)
+ size[..(child_idx - 1)].iter().copied().sum::<usize>()
} else {
0
}
}
}
}
/// Assert that this subtree satisfies our invariants, panicking if not.
fn check_invariants(&self, is_root: bool) {
match self {
Node::Leaf { data } => {
if !is_root {
assert!(data.len() >= B / 2);
}
}
Node::Internal { size, children } => {
assert_eq!(size.len(), children.len());
if !is_root {
assert!(size.len() >= B / 2);
}
for (child, size) in children.iter().zip(size) {
assert_eq!(child.subtree_size(), *size);
child.check_invariants(false);
}
}
}
}
}
impl<T, const B: usize> Default for TreeVec<T, B> {
fn default() -> Self {
Self {
root: Box::new(Node::Leaf {
data: ArrayVec::new(),
}),
}
}
}
impl<T, const B: usize> TreeVec<T, B> {
/// Constructs a new, empty, `TreeVec`.
///
/// This allocates a little; we don't have a special-case non-allocating
/// `TreeVec` for empty containers.
pub fn new() -> Self {
Self::default()
}
/// Returns true if we're empty.
pub fn is_empty(&self) -> bool {
match &*self.root {
Node::Leaf { data } => data.is_empty(),
Node::Internal { .. } => false,
}
}
/// Returns the number of elements in this container.
pub fn len(&self) -> usize {
self.root.subtree_size()
}
/// Gets a reference to the element at a specified index, or `None` if the
/// index is out-of-bounds.
///
/// Runtime is linear in the height of the tree (logarithmic in the length).
pub fn get(&self, index: usize) -> Option<&T> {
self.root.get(index)
}
/// Gets a mutable reference to the element at a specified index, or `None`
/// if the index is out-of-bounds.
///
/// Runtime is linear in the height of the tree (logarithmic in the length).
pub fn get_mut(&mut self, index: usize) -> Option<&mut T> {
self.root.get_mut(index)
}
/// Inserts an element at a specified index, or panics if the index is
/// out-of-bounds.
///
/// Runtime is linear in the height of the tree (logarithmic in the length),
/// and also linear in `B`.
pub fn insert(&mut self, index: usize, element: T) {
match self.root.insert(index, element) {
InsertResult::Done => {}
InsertResult::Split(node) => {
let mut root = Box::new(Node::Internal {
size: ArrayVec::new(),
children: ArrayVec::new(),
});
std::mem::swap(&mut root, &mut self.root);
let Node::Internal { size, children } = &mut *self.root else {
unreachable!();
};
size.push(root.subtree_size());
size.push(node.subtree_size());
children.push(root);
children.push(node);
}
}
}
/// Inserts an element at a specified index, shifting all later elements
/// back by 1.
///
/// Runtime is linear in the height of the tree (logarithmic in the length),
/// and also linear in `B`.
pub fn remove(&mut self, index: usize) -> T {
let ret = self.root.remove(index).into_inner();
if let Node::Internal { children, .. } = &mut *self.root
&& children.len() == 1
{
// unwrap: an internal node always has children
self.root = children.pop().unwrap()
}
ret
}
/// Asserts that this subtree satisfies our invariants, panicking if not.
pub fn check_invariants(&self) {
self.root.check_invariants(true);
}
/// Returns an iterator over all the elements in this container.
pub fn iter(&self) -> Iter<'_, T, B> {
let inner = match &*self.root {
Node::Leaf { data } => IterInner::Simple(data.iter()),
Node::Internal { .. } => {
let mut ret = TreeIter {
stack: Vec::new(),
leaf: [].iter(),
remaining: self.len(),
};
ret.descend(&*self.root);
IterInner::Tree(ret)
}
};
Iter { inner }
}
/// Returns an iterator over mutable references to all the elements in this
/// container.
pub fn iter_mut(&mut self) -> IterMut<'_, T, B> {
let len = self.len();
let inner = match &mut *self.root {
Node::Leaf { data } => IterMutInner::Simple(data.iter_mut()),
root @ Node::Internal { .. } => {
let mut ret = TreeIterMut {
stack: Vec::new(),
leaf: [].iter_mut(),
remaining: len,
};
ret.descend(root);
IterMutInner::Tree(ret)
}
};
IterMut { inner }
}
/// Returns the index of the first element for which `pred` returns false.
///
/// The above assumes that we're sorted in the sense that `pred` returns `true` for the first
/// bunch of elements and then `false` for the rest. If not, we don't necessarily return
/// the first offset for which `pred` if `false`, but we do return some offset where
/// `pred` is `false` but `pred` is `true` at `offset - 1`. (Here, corner cases are specified
/// by declaring that `pred` is `true` at offset `-1` and `false` at offset `len`.
pub fn partition_point<P>(&self, pred: P) -> usize
where
P: FnMut(&T) -> bool,
{
self.root.partition_point(pred)
}
/// Turns a generic range bounds into an inclusive start and an exclusive end.
///
/// This won't work if the range end is inclusive of `usize::MAX`, so don't do that.
fn normalize_bounds(&self, range: impl std::ops::RangeBounds<usize>) -> (usize, usize) {
let start = match range.start_bound() {
std::ops::Bound::Included(x) => *x,
std::ops::Bound::Excluded(x) => *x + 1,
std::ops::Bound::Unbounded => 0,
};
let end = match range.end_bound() {
std::ops::Bound::Included(x) => *x + 1,
std::ops::Bound::Excluded(x) => *x,
std::ops::Bound::Unbounded => self.len(),
};
(start, end)
}
/// Returns an iterator over a sub-range of this container.
pub fn range(&self, range: impl std::ops::RangeBounds<usize>) -> Iter<'_, T, B> {
let (start, end) = self.normalize_bounds(range);
let inner = match &*self.root {
Node::Leaf { data } => IterInner::Simple(data[start..end].iter()),
Node::Internal { .. } => {
if end > self.len() {
panic!("out of bounds");
}
let mut ret = TreeIter {
stack: Vec::new(),
leaf: [].iter(),
remaining: end - start,
};
ret.descend_to(&*self.root, start);
IterInner::Tree(ret)
}
};
Iter { inner }
}
/// Returns an iterator over mutable references to a sub-range of this container.
pub fn range_mut(&mut self, range: impl std::ops::RangeBounds<usize>) -> IterMut<'_, T, B> {
let (start, end) = self.normalize_bounds(range);
let len = self.len();
let inner = match &mut *self.root {
Node::Leaf { data } => IterMutInner::Simple(data[start..end].iter_mut()),
root @ Node::Internal { .. } => {
if end > len {
panic!("out of bounds");
}
let mut ret = TreeIterMut {
stack: Vec::new(),
leaf: [].iter_mut(),
remaining: end - start,
};
ret.descend_to(root, start);
IterMutInner::Tree(ret)
}
};
IterMut { inner }
}
}
impl<T, const B: usize> FromIterator<T> for TreeVec<T, B> {
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
// TODO: a faster implementation, maybe? This is only
// used in tests right now.
let mut ret = TreeVec::new();
for (idx, x) in iter.into_iter().enumerate() {
ret.insert(idx, x);
}
ret
}
}
impl<T, const B: usize> std::ops::Index<usize> for TreeVec<T, B> {
type Output = T;
fn index(&self, index: usize) -> &T {
self.get(index).unwrap()
}
}
impl<T, const B: usize> std::ops::IndexMut<usize> for TreeVec<T, B> {
fn index_mut(&mut self, index: usize) -> &mut T {
self.get_mut(index).unwrap()
}
}
/// An iterator over elements of a [`TreeVec`].
pub struct Iter<'a, T, const B: usize> {
inner: IterInner<'a, T, B>,
}
// We special-case a fast-path for a tree with height 1.
//
// This, along with some inline annotations, makes a measurable performance
// difference.
enum IterInner<'a, T, const B: usize> {
Simple(std::slice::Iter<'a, T>),
Tree(TreeIter<'a, T, B>),
}
struct TreeIter<'a, T, const B: usize> {
// If you imagine how the recursive implementation of a tree iteration
// works, the local state at each intermediate call consists of a
// partially-finished iteration over the children of that internal node.
// This stores the stack of that imaginary recursive implementation.
stack: Vec<std::slice::Iter<'a, Box<Node<T, B>>>>,
leaf: std::slice::Iter<'a, T>,
// The number of remaining elements; used for iteration over ranges. The
// full-iteration implementation doesn't need it, but it isn't worth having
// two different iterator implementations.
remaining: usize,
}
impl<'a, T, const B: usize> TreeIter<'a, T, B> {
// Fill out the rest of the stack and the leaf, by descending
// to the first descendent of `node`.
fn descend(&mut self, mut node: &'a Node<T, B>) {
loop {
match node {
Node::Leaf { data } => {
self.leaf = data.iter();
return;
}
Node::Internal { children, .. } => {
let mut children = children.iter();
// unwrap: internal nodes are always non-empty
node = children.next().unwrap();
self.stack.push(children);
}
}
}
}
// Fill out the rest of the stack and the leaf, by descending
// to the node at offset `offset` in the subtree rooted at `node`.
fn descend_to(&mut self, mut node: &'a Node<T, B>, mut offset: usize) {
loop {
match node {
Node::Leaf { data } => {
self.leaf = data[offset..].iter();
return;
}
Node::Internal { children, size } => {
let Some((idx, child_offset)) = child_idx(size, offset) else {
return;
};
offset = child_offset;
let mut children = children[idx..].iter();
// unwrap: child_idx always returns a valid index into children
node = children.next().unwrap();
self.stack.push(children);
}
}
}
}
fn next(&mut self) -> Option<&'a T> {
if self.remaining == 0 {
None
} else {
self.remaining -= 1;
if let Some(ret) = self.leaf.next() {
Some(ret)
} else {
// The current leaf is exhausted. Unwind the stack until we find
// a non-exhausted internal node, then descend again.
loop {
let stack_top = self.stack.last_mut()?;
let Some(next_node) = stack_top.next() else {
self.stack.pop();
continue;
};
self.descend(next_node);
return self.leaf.next();
}
}
}
}
}
impl<'a, T, const B: usize> Iterator for Iter<'a, T, B> {
type Item = &'a T;
#[inline(always)]
fn next(&mut self) -> Option<Self::Item> {
match &mut self.inner {
IterInner::Simple(it) => it.next(),
IterInner::Tree(it) => it.next(),
}
}
}
/// An iterator over mutable references elements of a [`TreeVec`].
pub struct IterMut<'a, T, const B: usize> {
inner: IterMutInner<'a, T, B>,
}
enum IterMutInner<'a, T, const B: usize> {
Simple(std::slice::IterMut<'a, T>),
Tree(TreeIterMut<'a, T, B>),
}
// This is basically a copy-pasted of `TreeIter`, but with non-mut things
// turned into mut things. Right now there's only the one duplication so
// is isn't worth trying to build an abstraction.
struct TreeIterMut<'a, T, const B: usize> {
stack: Vec<std::slice::IterMut<'a, Box<Node<T, B>>>>,
leaf: std::slice::IterMut<'a, T>,
remaining: usize,
}
impl<'a, T, const B: usize> TreeIterMut<'a, T, B> {
fn descend(&mut self, mut node: &'a mut Node<T, B>) {
loop {
match node {
Node::Leaf { data } => {
self.leaf = data.iter_mut();
return;
}
Node::Internal { children, .. } => {
let mut children = children.iter_mut();
// unwrap: internal nodes are always non-empty
node = children.next().unwrap();
self.stack.push(children);
}
}
}
}
fn descend_to(&mut self, mut node: &'a mut Node<T, B>, mut offset: usize) {
loop {
match node {
Node::Leaf { data } => {
self.leaf = data[offset..].iter_mut();
return;
}
Node::Internal { children, size } => {
let Some((idx, child_offset)) = child_idx(size, offset) else {
return;
};
offset = child_offset;
let mut children = children[idx..].iter_mut();
// unwrap: child_idx always returns a valid index into children
node = children.next().unwrap();
self.stack.push(children);
}
}
}
}
fn next(&mut self) -> Option<&'a mut T> {
if self.remaining == 0 {
None
} else {
self.remaining -= 1;
if let Some(ret) = self.leaf.next() {
Some(ret)
} else {
loop {
let stack_top = self.stack.last_mut()?;
let Some(next_node) = stack_top.next() else {
self.stack.pop();
continue;
};
self.descend(next_node);
return self.leaf.next();
}
}
}
}
}
impl<'a, T, const B: usize> Iterator for IterMut<'a, T, B> {
type Item = &'a mut T;
#[inline(always)]
fn next(&mut self) -> Option<Self::Item> {
match &mut self.inner {
IterMutInner::Simple(iter) => iter.next(),
IterMutInner::Tree(tree_iter) => tree_iter.next(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn insert_get() {
let mut vec = TreeVec::<i32, 4>::default();
vec.insert(0, 1);
vec.insert(0, 2);
vec.insert(0, 3);
vec.insert(0, 4);
assert_eq!(*vec.get(0).unwrap(), 4);
assert_eq!(*vec.get(1).unwrap(), 3);
assert_eq!(*vec.get(2).unwrap(), 2);
assert_eq!(*vec.get(3).unwrap(), 1);
vec.check_invariants();
vec.insert(0, 1);
vec.insert(0, 2);
vec.insert(0, 3);
vec.insert(0, 4);
vec.check_invariants();
assert_eq!(*vec.get(0).unwrap(), 4);
assert_eq!(*vec.get(1).unwrap(), 3);
assert_eq!(*vec.get(2).unwrap(), 2);
assert_eq!(*vec.get(3).unwrap(), 1);
assert_eq!(*vec.get(4).unwrap(), 4);
assert_eq!(*vec.get(5).unwrap(), 3);
assert_eq!(*vec.get(6).unwrap(), 2);
assert_eq!(*vec.get(7).unwrap(), 1);
}
#[test]
fn insert_remove() {
let mut vec = TreeVec::<i32, 4>::default();
vec.insert(0, 1);
vec.insert(0, 2);
vec.insert(0, 3);
vec.insert(0, 4);
vec.remove(1);
vec.check_invariants();
assert_eq!(*vec.get(0).unwrap(), 4);
assert_eq!(*vec.get(1).unwrap(), 2);
assert_eq!(*vec.get(2).unwrap(), 1);
vec.insert(0, 1);
vec.insert(0, 2);
vec.insert(0, 3);
vec.insert(0, 4);
vec.remove(5);
vec.check_invariants();
assert_eq!(*vec.get(0).unwrap(), 4);
assert_eq!(*vec.get(1).unwrap(), 3);
assert_eq!(*vec.get(2).unwrap(), 2);
assert_eq!(*vec.get(3).unwrap(), 1);
assert_eq!(*vec.get(4).unwrap(), 4);
assert_eq!(*vec.get(5).unwrap(), 1);
}
#[test]
fn iter() {
let mut vec = TreeVec::<i32, 4>::default();
assert_eq!(vec.iter().cloned().collect::<Vec<_>>(), Vec::<i32>::new());
vec.insert(0, 1);
assert_eq!(vec.iter().cloned().collect::<Vec<_>>(), vec![1]);
vec.insert(1, 2);
assert_eq!(vec.iter().cloned().collect::<Vec<_>>(), vec![1, 2]);
vec.insert(0, 1);
vec.insert(0, 1);
vec.insert(0, 1);
vec.insert(0, 1);
vec.insert(0, 1);
assert_eq!(
vec.iter().cloned().collect::<Vec<_>>(),
vec![1, 1, 1, 1, 1, 1, 2]
);
}
}
+129
View File
@@ -0,0 +1,129 @@
/// Implement methods for vectors indexed with typed indices.
///
/// We use a lot of indices into arrays: we have arrays indexed by input
/// segments, arrays indexed by output segments, arrays indexed by output
/// endpoints, and more. To help keep these straight, we wrap indices
/// in new types, like `SegIdx`, `OutputSegIdx`, etc., and we wrap `Vec`
/// in new types, like `SegVec`, `OutputSegVec`, etc. Then you can only
/// use the right index type with the right vector type.
///
/// This macro implements a bunch of useful methods on the new-type
/// wrappers. It assumes that the index new-type is just a wrapper
/// around `usize`.
macro_rules! impl_typed_vec {
($vec_name:ident, $idx_name:ident, $dbg_prefix:expr_2021) => {
impl std::fmt::Debug for $idx_name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}_{}", $dbg_prefix, self.0)
}
}
#[allow(dead_code)]
impl<T> $vec_name<T> {
/// Wraps a plain vector.
pub fn from_vec(vec: Vec<T>) -> Self {
Self { inner: vec }
}
/// Creates a new vector with capacity for at least `cap` output segments before reallocating.
pub fn with_capacity(cap: usize) -> Self {
Self {
inner: Vec::with_capacity(cap),
}
}
/// Returns an iterator over all indices into this vector.
pub fn indices(&self) -> impl Iterator<Item = $idx_name> + use<T> {
(0..self.inner.len()).map($idx_name)
}
/// The length of this vector.
pub fn len(&self) -> usize {
self.inner.len()
}
/// Are we empty?
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
/// Adds a new element, returning its index.
pub fn push(&mut self, elt: T) -> $idx_name {
self.inner.push(elt);
$idx_name(self.len() - 1)
}
/// Returns an iterator over indices and elements.
pub fn iter(&self) -> impl Iterator<Item = ($idx_name, &T)> + '_ {
self.inner
.iter()
.enumerate()
.map(|(idx, t)| ($idx_name(idx), t))
}
/// Returns an iterator over indices and mutable elements.
pub fn iter_mut(&mut self) -> impl Iterator<Item = ($idx_name, &mut T)> + '_ {
self.inner
.iter_mut()
.enumerate()
.map(|(idx, t)| ($idx_name(idx), t))
}
}
#[allow(dead_code)]
impl<T: Default> $vec_name<T> {
/// Creates a new vector with `size` elements, each initialized with the default value.
pub fn with_size(size: usize) -> Self {
Self {
inner: std::iter::from_fn(|| Some(T::default()))
.take(size)
.collect(),
}
}
}
impl<T> Default for $vec_name<T> {
fn default() -> Self {
Self { inner: Vec::new() }
}
}
impl<T> std::ops::Index<$idx_name> for $vec_name<T> {
type Output = T;
fn index(&self, index: $idx_name) -> &Self::Output {
&self.inner[index.0]
}
}
impl<T> std::ops::IndexMut<$idx_name> for $vec_name<T> {
fn index_mut(&mut self, index: $idx_name) -> &mut T {
&mut self.inner[index.0]
}
}
impl<T: std::fmt::Debug> std::fmt::Debug for $vec_name<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
struct Entry<'a, T> {
idx: $idx_name,
inner: &'a T,
}
impl<T: std::fmt::Debug> std::fmt::Debug for Entry<'_, T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{idx:?}: {inner:?}", idx = self.idx, inner = self.inner,)
}
}
let mut list = f.debug_list();
for idx in self.indices() {
list.entry(&Entry {
idx,
inner: &self[idx],
});
}
list.finish()
}
}
};
}
+56
View File
@@ -0,0 +1,56 @@
use std::path::PathBuf;
use kurbo::BezPath;
use libtest_mimic::{Arguments, Failed, Trial};
use linesweeper::topology::Topology;
use linesweeper_util::outline_to_bezpath;
use skrifa::{FontRef, MetadataProvider};
fn main() {
let args = Arguments::from_args();
let tests = glyph_tests();
libtest_mimic::run(&args, tests).exit();
}
fn glyph_tests() -> Vec<Trial> {
let ws = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let paths = glob::glob(&format!("{ws}/tests/fonts/**/*.ttf")).unwrap();
paths
.into_iter()
.flat_map(|p| {
let p = p.unwrap();
let name = p.strip_prefix(&ws).unwrap().display().to_string();
font_glyph_tests(name, p)
})
.collect()
}
fn font_glyph_tests(name: String, path: PathBuf) -> Vec<Trial> {
let data = std::fs::read(&path).unwrap();
let font_ref = FontRef::new(&data).unwrap();
let names = font_ref.glyph_names();
font_ref
.outline_glyphs()
.iter()
.zip(font_ref.outline_glyphs().iter().skip(1))
.map(|((id1, outline1), (_id2, outline2))| {
let glyph_name = names.get(id1).unwrap();
let path1 = outline_to_bezpath(outline1);
let path2 = outline_to_bezpath(outline2);
Trial::test(format!("{name}-{glyph_name}"), || {
test_outline(path1, path2)
})
})
.collect()
}
fn test_outline(path1: BezPath, path2: BezPath) -> Result<(), Failed> {
if path1.is_empty() || path2.is_empty() {
return Ok(());
}
let top = Topology::from_paths_binary(&path1, &path2, 1e-3).unwrap();
let _out_paths = top.compute_positions();
Ok(())
}
+222
View File
@@ -0,0 +1,222 @@
use kurbo::{BezPath, PathEl, Shape};
use libtest_mimic::{Arguments, Failed, Trial};
use linesweeper::{BinaryOp, FillRule, binary_op};
use serde::Deserialize;
use std::fs;
fn main() {
let args = Arguments::from_args();
let tests = paperjs_tests();
libtest_mimic::run(&args, tests).exit();
}
fn paperjs_tests() -> Vec<Trial> {
let mut tests = Vec::new();
// Load all test files
let test_files = vec![
("boolean-test/data/intersection.json", "intersection"),
("boolean-test/data/union.json", "union"),
("boolean-test/data/subtraction.json", "subtraction"),
];
for (file_path, op_name) in test_files {
if let Ok(file_tests) = load_test_file(file_path, op_name) {
tests.extend(file_tests);
}
}
tests
}
fn load_test_file(
file_path: &str,
op_name: &str,
) -> Result<Vec<Trial>, Box<dyn std::error::Error>> {
let contents = fs::read_to_string(file_path)?;
let test_file: PaperJsTestFile = serde_json::from_str(&contents)?;
let operation = get_binary_op(&test_file.operation)
.ok_or_else(|| format!("Unknown operation: {}", test_file.operation))?;
let mut trials = Vec::new();
for test_set in test_file.tests {
let set_a = test_set.op1.clone();
for case in test_set.cases {
let test_name = format!("{}/{}:{}", op_name, test_set.name, case.name);
let test_name_path = format!("{}__{}__{}", op_name, test_set.name, case.name);
let set_a = set_a.clone();
let set_b = case.op2.clone();
let expected = case.res.clone();
trials.push(Trial::test(test_name, move || {
run_test_case(&test_name_path, &set_a, &set_b, &expected, operation)
}));
}
}
Ok(trials)
}
#[derive(Debug, Deserialize)]
struct PaperJsTestFile {
#[serde(rename = "fn")]
operation: String,
tests: Vec<TestSet>,
}
#[derive(Debug, Deserialize)]
struct TestSet {
name: String,
op1: String, // set_a in SVG format
cases: Vec<TestCase>,
}
#[derive(Debug, Deserialize)]
struct TestCase {
name: String,
op2: String, // set_b in SVG format
res: String, // expected result in SVG format
}
/// Convert SVG string to BezPath
fn svg_to_path(svg: &str) -> Result<BezPath, Box<dyn std::error::Error>> {
BezPath::from_svg(svg).map_err(|e| format!("Failed to parse SVG path: {}", e).into())
}
/// Convert Contours to a single BezPath by concatenating all contour paths
fn contours_to_bezpath(contours: &linesweeper::topology::Contours) -> BezPath {
let mut combined = BezPath::new();
for contour in contours.contours() {
combined.extend(contour.path.clone());
}
combined
}
/// Map paper.js operation names to BinaryOp enum
fn get_binary_op(operation: &str) -> Option<BinaryOp> {
match operation {
"intersect" => Some(BinaryOp::Intersection),
"unite" | "union" => Some(BinaryOp::Union),
"subtract" | "difference" => Some(BinaryOp::Difference),
"exclude" | "xor" => Some(BinaryOp::Xor),
_ => None,
}
}
fn to_tiny_skia(p: &BezPath) -> Option<tiny_skia::Path> {
let mut b = tiny_skia::PathBuilder::new();
for el in p.elements() {
match el {
PathEl::MoveTo(p) => b.move_to(p.x as f32, p.y as f32),
PathEl::LineTo(p) => b.line_to(p.x as f32, p.y as f32),
PathEl::QuadTo(p0, p1) => b.quad_to(p0.x as f32, p0.y as f32, p1.x as f32, p1.y as f32),
PathEl::CurveTo(p0, p1, p2) => b.cubic_to(
p0.x as f32,
p0.y as f32,
p1.x as f32,
p1.y as f32,
p2.x as f32,
p2.y as f32,
),
PathEl::ClosePath => b.close(),
}
}
b.finish()
}
#[cfg(feature = "debug-svg")]
fn dump_svg(file_path: &str, svg: &BezPath) {
let bbox = svg.bounding_box().inset(2.0);
let doc = svg::Document::new().set(
"viewBox",
(bbox.min_x(), bbox.min_y(), bbox.width(), bbox.height()),
);
let path = svg::node::element::Path::new()
.set("d", svg.to_svg())
.set("stroke-width", "0")
.set("fill", "black");
let doc = doc.add(path);
svg::save(file_path, &doc).unwrap();
}
fn pixmap_distance(a: &tiny_skia::Pixmap, b: &tiny_skia::Pixmap) -> f32 {
let w = a.width().min(b.width());
let h = a.height().min(b.height());
let mut diff = 0.0;
for x in 0..w {
for y in 0..h {
let ac = a.pixel(x, y).unwrap();
let bc = b.pixel(x, y).unwrap();
let rdiff = ac.red().abs_diff(bc.red());
let gdiff = ac.green().abs_diff(bc.green());
let bdiff = ac.blue().abs_diff(bc.blue());
diff += rdiff.max(gdiff).max(bdiff) as f32 / 255.0;
}
}
diff / (w * h) as f32
}
fn path_to_pixmap(bbox: kurbo::Rect, path: &BezPath) -> tiny_skia::Pixmap {
let size = 256;
let scale = size as f64 / bbox.width().max(bbox.height());
let transform =
tiny_skia::Transform::from_translate(-bbox.min_x() as f32, -bbox.min_y() as f32)
.post_scale(scale as f32, scale as f32);
let mut pixmap = tiny_skia::Pixmap::new(256, 256).unwrap();
let mut paint = tiny_skia::Paint::default();
paint.set_color(tiny_skia::Color::BLACK);
if let Some(p) = to_tiny_skia(path) {
pixmap.fill_path(&p, &paint, tiny_skia::FillRule::EvenOdd, transform, None);
}
pixmap
}
/// Run a single test case
fn run_test_case(
test_name_path: &str,
set_a_svg: &str,
set_b_svg: &str,
expected_svg: &str,
operation: BinaryOp,
) -> Result<(), Failed> {
let set_a = svg_to_path(set_a_svg).map_err(|e| format!("Failed to parse set_a: {}", e))?;
let set_b = svg_to_path(set_b_svg).map_err(|e| format!("Failed to parse set_b: {}", e))?;
let expected =
svg_to_path(expected_svg).map_err(|e| format!("Failed to parse expected result: {}", e))?;
// Run binary operation
// If it's subtraction, we need to swap the order of the operands because paper.js does A - B but our library does A - B as well, so we need to do B - A to match paper.js results
let (set_a, set_b) = if operation == BinaryOp::Difference {
(set_b, set_a)
} else {
(set_a, set_b)
};
let result = binary_op(&set_a, &set_b, FillRule::NonZero, operation)
.map_err(|e| format!("Binary operation failed: {:?}", e))?;
let result_path = contours_to_bezpath(&result).reverse_subpaths();
let bbox = set_a.bounding_box().union(set_b.bounding_box());
let our_pixmap = path_to_pixmap(bbox, &result_path);
let expected_pixmap = path_to_pixmap(bbox, &expected);
let diff = pixmap_distance(&our_pixmap, &expected_pixmap);
if diff > 0.0 {
#[cfg(feature = "debug-svg")]
{
dump_svg(&format!("ignore/{test_name_path}_set_a.svg"), &set_a);
dump_svg(&format!("ignore/{test_name_path}_set_b.svg"), &set_b);
dump_svg(&format!("ignore/{test_name_path}_expected.svg"), &expected);
dump_svg(&format!("ignore/{test_name_path}_result.svg"), &result_path);
}
// Silence the unused variable warning
#[cfg(not(feature = "debug-svg"))]
let _ = test_name_path;
return Err(format!("pixmap distance {diff}, above threshold").into());
}
Ok(())
}
+46
View File
@@ -0,0 +1,46 @@
use kurbo::BezPath;
use libtest_mimic::{Arguments, Failed, Trial};
use linesweeper::binary_op;
use std::path::{Path, PathBuf};
fn main() {
let args = Arguments::from_args();
let tests = regression_tests();
libtest_mimic::run(&args, tests).exit();
}
fn regression_tests() -> Vec<Trial> {
let ws = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let paths = glob::glob(&format!("{ws}/tests/regression/**/*.txt")).unwrap();
paths
.into_iter()
.map(|p| {
let p = p.unwrap();
let name = input_path_base(&p).display().to_string();
Trial::test(name, || generate_regression_test(p))
})
.collect()
}
fn input_path_base(input_path: &Path) -> &Path {
let ws = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let base = format!("{ws}/tests/regression");
input_path.strip_prefix(base).unwrap()
}
fn generate_regression_test(path: PathBuf) -> Result<(), Failed> {
let input = std::fs::read_to_string(&path).unwrap();
let lines: Vec<_> = input.lines().collect();
assert_eq!(lines.len(), 2);
let p0 = BezPath::from_svg(lines[0]).unwrap();
let p1 = BezPath::from_svg(lines[1]).unwrap();
binary_op(
&p0,
&p1,
linesweeper::FillRule::EvenOdd,
linesweeper::BinaryOp::Xor,
)
.unwrap();
Ok(())
}
+2
View File
@@ -0,0 +1,2 @@
M160.2,-48 C208.7810588057864,-47.99999999999999 253.30168848946408,-20.527885941061772 275.03250516799596,22.933747416002024 L298.93250516799594,70.73374741600202 L213.06749483200403,70.73374741600205 L236.967494832004,22.933747416002035 C248.82297877016197,-0.7772204603138491 277.65528470768203,-10.387989106153874 301.36625258399795,1.467494832004057 C325.0772204603138,13.322978770161992 334.68798910615385,42.15528470768207 322.8325051679959,65.86625258399796 L298.93250516799594,113.66625258399797 C290.8016884894641,129.9278859410618 274.1810588057864,140.2 256,140.2 C237.81894119421364,140.2 221.19831151053594,129.9278859410618 213.06749483200403,113.66625258399799 L189.16749483200402,65.86625258399798 C183.69831151053594,54.92788594106179 172.4189411942136,47.99999999999999 160.2,48 C133.6903320081219,48 112.19999999999999,26.509667991878086 112.19999999999999,0.0000000000000029391523179536475 C112.19999999999999,-26.509667991878075 133.6903320081219,-47.99999999999999 160.2,-48
M232.1,44.4 L256,92.2 L279.9,44.4 Z
+2
View File
@@ -0,0 +1,2 @@
M27.30779709,-257.85907878 C27.30779665,-257.85907835 27.307796200000002,-257.85907792 27.30779576,-257.8590775 C27.292172620000002,-257.8439677 -109.98458240000001,-125.07781586 -233.88593633,-5.24757973 C-233.88593684,-5.24757923 -233.88593736,-5.24757873 -233.88593787,-5.24757824 C-233.88593736,-5.2475777400000005 -233.88593684,-5.24757724 -233.88593632,-5.24757675 C-174.52579263,51.894883570000005 -115.13821436,109.22164426 -77.72402204000001,145.36173051 C-77.72402157,145.36173091 -77.72402116,145.3617313 -77.7240208,145.3617317 C-77.7240208,145.3617317 -77.72402077,145.3617317 -77.72402077,145.3617317 C-77.72401908,145.3617313 -77.72401740000001,145.36173091 -77.72401572,145.36173051 C-1.60745976,127.42335802000001 72.89114167,108.36966443 101.12619937000001,101.0791851 C101.12620127,101.07918461 101.12620318,101.07918412000001 101.12620509,101.07918362000001 C101.1262059,101.07918313 101.12620674,101.07918264 101.1262076,101.07918215000001 C105.56968257,98.47134695 110.06808693,95.83127419 114.61338829,93.16367801 C114.61338927,93.16367744 114.61339025000001,93.16367686 114.61339123,93.16367628 C114.61339123,93.16367628 114.61339123,93.16367628 114.61339123,93.16367628 C114.61339142,93.16367571 114.61339161000001,93.16367513 114.6133918,93.16367456 C123.52785808,66.12754849 166.90764887,-69.59356743000001 182.19132273,-195.37502561 C182.19132278,-195.37502607 182.19132284,-195.37502653 182.19132290000002,-195.37502699 C182.19132175000001,-195.37502746 182.19132061,-195.37502792 182.19131946000002,-195.37502838 C99.03601942,-228.9220441 27.32536748,-257.85199043 27.307800320000002,-257.8590775 C27.30779923,-257.85907792 27.30779816,-257.85907835 27.30779709,-257.85907878 L27.30779709,-257.85907878 Z M-408.62463824,-172.46325538 C-408.62463827,-172.46325472 -408.62463831,-172.46325406 -408.62463834,-172.4632534 C-414.02520486000003,-63.40231834 -377.28160484,34.20037327 -342.02221595000003,99.33558422 C-342.02221565,99.33558477 -342.02221536,99.33558531 -342.02221506,99.33558586000001 C-342.02221506,99.33558586000001 -342.02221506,99.33558586000001 -342.02221506,99.33558586000001 C-342.0222145,99.33558531 -342.02221393,99.33558477 -342.02221337000003,99.33558422 C-310.40978596,68.76186957 -272.80781456,32.395413760000004 -233.88593941,-5.24757675 C-233.8859389,-5.24757724 -233.88593838,-5.2475777400000005 -233.88593787,-5.24757824 C-233.88593787,-5.24757824 -233.88593787,-5.24757824 -233.88593787,-5.24757824 C-233.88593839,-5.24757873 -233.88593891,-5.24757923 -233.88593942,-5.24757973 C-304.53464918000003,-73.25686748 -375.14449713,-141.00509201 -408.62463617000003,-172.4632534 C-408.62463687,-172.46325406 -408.62463756,-172.46325472 -408.62463824,-172.46325538 L-408.62463824,-172.46325538 Z
M27.3077958,-257.85907753 C27.2923816,-257.84416980000003 -109.98448809,-125.07790706 -233.88593634,-5.24757971 C-233.88593634,-5.24757971 -233.88593825,-5.24757971 -233.88593825,-5.24757971 C-233.88593941,-5.24757971 -233.88593942,-5.24757972 -233.88593942,-5.24757972 C-304.53459173,-73.25681217 -375.1443823,-141.00498183 -408.62463995,-172.46317667 C-408.62464066,-172.46317733 -408.62464135,-172.46317798 -408.62464203,-172.46317863000002 C-408.62464208,-172.46317798 -408.62464211,-172.46317733 -408.62464214,-172.46317667 C-414.02518763,-63.40227258 -377.28159657000003,34.20038853 -342.02221596,99.3355842 C-342.02221566000003,99.33558476 -342.02221536,99.33558531 -342.02221506,99.33558586000001 C-342.02221449,99.33558531 -342.02221392,99.33558476 -342.02221335,99.3355842 C-310.40978594,68.76186955 -272.80781455,32.39541375 -233.88593941,-5.24757675 C-233.88593941,-5.24757675 -233.88593941,-5.24757675 -233.88593941,-5.24757675 C-233.88593889,-5.24757724 -233.88593838,-5.2475777400000005 -233.88593787,-5.24757823 C-233.88593735,-5.2475777400000005 -233.88593684,-5.24757724 -233.88593632,-5.24757675 C-233.88593632,-5.24757675 -233.88593632,-5.24757675 -233.88593632,-5.24757675 C-174.52579259,51.89488361 -115.13821427,109.22164435 -77.72402196,145.36173056 C-77.72402157,145.36173094 -77.72402117,145.36173132 -77.72402078,145.3617317 C-77.72401917,145.36173132 -77.72401755,145.36173094 -77.72401594,145.36173056 C-1.60745982,127.42335803 72.89114182,108.36966439 101.12619946,101.07918507000001 C101.12620136,101.07918458 101.12620325,101.07918409 101.12620514,101.07918360000001 C101.12620595,101.07918311 101.12620679,101.07918263 101.12620764,101.07918214 C105.56968257,98.47134695 110.0680869,95.83127422 114.61338822,93.16367806 C114.61338923,93.16367747 114.61339023000001,93.16367689 114.61339123,93.1636763 C114.61339142,93.16367571 114.61339161000001,93.16367512000001 114.61339181,93.16367453000001 C123.52785811,66.12754843 166.90764887,-69.59356747 182.19132273,-195.37502563 C182.19132279000002,-195.37502608 182.19132285,-195.37502654 182.19132289,-195.37502699 C182.19132177,-195.37502745 182.19132064000001,-195.3750279 182.1913195,-195.37502836000002 C99.03588674,-228.92209763 27.32513858,-257.85208278 27.30780024,-257.85907753 C27.30780024,-257.85907753 27.30779791,-257.85907753 27.30779791,-257.85907753 C27.30779791,-257.85907753 27.3077958,-257.85907753 27.3077958,-257.85907753 L27.3077958,-257.85907753 Z
+2
View File
@@ -0,0 +1,2 @@
M-51,-390 C-51.00000037,-389.99999955 -51.00000075,-389.99999909 -51.00000112,-389.99999864 C-51.0133208,-389.98380542 -169.63665568,-245.76914195 -276.70205531,-115.60587692 C-276.70205486000003,-115.60587746 -276.70205486000003,-115.60587746 -276.70205531,-115.60587692 C-276.70205486000003,-115.60587638 -276.70205442,-115.60587585 -276.70205397,-115.60587531 C-192.38931516,-13.58098944 -108.0125259,88.98477391 -108.00000139000001,88.99999842 C-108.00000091,88.99999895 -108.00000044000001,88.99999947 -108,89 C-107.99999928,88.99999947 -107.99999924000001,88.99999947 -107.9999999,89 C-107.99266176,88.99458625 300,-212 300,-212 C300,-212 -50.96433872,-389.98191536 -50.99999728,-389.99999864 C-50.9999982,-389.99999909 -50.999999100000004,-389.99999955 -51,-390 L-51,-390 Z M-445,-317 C-445.000001,-316.99999994 -445.00000199,-316.99999988 -445.00000299,-316.99999981 C-445.00000199,-316.99999976 -445.00000193,-316.99999971 -445.0000028,-316.99999965 C-445.00000193,-316.9999996 -445.00000188,-316.99999956 -445.00000264,-316.99999951 C-445.00000188,-316.99999943 -445.00000216,-316.99999934 -445.0000035,-316.99999926 C-445.00000216,-316.9999992 -445.000002,-316.99999914 -445.000003,-316.99999907 C-445.000002,-316.99999903 -445.00000187,-316.99999898 -445.00000262000003,-316.99999893 C-445.00000187,-316.99999886 -445.00000206,-316.99999879 -445.00000319000003,-316.99999872 C-445.00000206,-316.99999865 -445.00000206,-316.99999858 -445.00000319000003,-316.99999851 C-445.07164839,-316.99554979 -606,-307 -606,-307 C-606,-307 -445.00289573000003,88.99287759 -445,89 C-445.00000023,88.99999940000001 -445.00000023,88.99999940000001 -445,89 C-444.99999951,88.99999940000001 -444.99999901,88.9999988 -444.99999852,88.99999820000001 C-444.98741208,88.98469641 -364.75335505,-8.5587384 -276.70205664,-115.60587531 C-276.70205619,-115.60587585 -276.70205575,-115.60587638 -276.70205531,-115.60587692 C-276.70205531,-115.60587692 -276.70205531,-115.60587692 -276.70205531,-115.60587692 C-276.70205576,-115.60587746 -276.7020562,-115.605878 -276.70205665000003,-115.60587854 C-360.13048219,-216.56067745000001 -443.49619297,-316.98588429 -444.99987267,-316.99999851 C-444.99987444,-316.99999858 -444.99987444,-316.99999865 -444.99987267,-316.99999872 C-444.99987444,-316.99999879 -444.99987601000004,-316.99999886 -444.99987738,-316.99999893 C-444.99988877,-316.99999898 -444.99988744,-316.99999903 -444.99987339,-316.99999907 C-444.9998795,-316.99999914 -444.99987949,-316.9999992 -444.99987336,-316.99999926 C-444.99986538,-316.99999934 -444.99986663,-316.99999943 -444.99987712,-316.99999951 C-444.99988835,-316.99999956 -444.99988766,-316.9999996 -444.99987503,-316.99999965 C-444.99988443,-316.99999971 -444.9998839,-316.99999976 -444.99987344,-316.99999981 C-444.99987969,-316.99999988 -444.99992188,-316.99999994 -445,-317 L-445,-317 Z
M-147.77035068,-523.63725234 C-147.77897268,-523.63725228 -147.78328911,-523.63725221 -147.78329996,-523.63725215 C-147.78278455,-523.6372520900001 -147.78276705,-523.63725204 -147.78324747,-523.63725198 C-147.78238075000002,-523.63725194 -147.78233785,-523.63725189 -147.78311876,-523.63725184 C-147.78494057,-523.63725176 -147.78487849,-523.63725167 -147.78293254,-523.63725159 C-147.78318771,-523.6372515200001 -147.78331018,-523.63725146 -147.78329996,-523.6372514 C-147.78235912,-523.63725135 -147.78229246,-523.6372513 -147.78309999,-523.63725125 C-147.78382096,-523.63725118 -147.78386999,-523.63725111 -147.78324709,-523.63725104 C-147.78386999,-523.63725097 -147.78386999,-523.6372509 -147.78324709,-523.63725083 C-290.97059178,-523.62540865 -459.36114997,-454.02730629 -459.36114997,-454.02730629 C-503.78067701000003,-360.84914474 -515.91201353,-267.22250217 -511.28673862,-182.54906502 C-501.51553210000003,-3.67069389 -416.96053353,135.25003698 -404.05763535,145.57150696 C-402.79147715,146.58435025 -400.17221075000003,147.05752789000002 -396.38569434,147.05843347 C-396.38546768000003,147.05843352 -396.3852332,147.05843358 -396.3849909,147.05843363 C-396.38523229000003,147.05843368 -396.3852474,147.05843373 -396.38503624000003,147.05843377 C-396.38525358,147.05843386 -396.38517355,147.05843394000001 -396.38479617,147.05843402 C-396.38516954,147.05843409 -396.38521642,147.05843415 -396.38493682,147.05843421 C-396.38521417,147.05843426 -396.38524896,147.05843430000002 -396.38504119,147.05843435 C-396.38525258,147.05843442 -396.38520028,147.05843449 -396.38488428,147.05843456 C-396.38520028,147.05843463 -396.38183866,147.0584347 -396.37479941,147.05843477 C-396.36776496,147.0584347 -396.36439327,147.05843463 -396.36468434,147.05843456 C-396.36439327,147.05843449 -396.36434448,147.05843442 -396.36453799000003,147.05843435 C-396.36434177,147.05843430000002 -396.36437435,147.05843426 -396.36463574,147.05843421 C-396.36437604,147.05843415 -396.36441911000003,147.05843409 -396.36476495,147.05843402 C-396.36442211,147.05843394000001 -396.36434802,147.05843386 -396.36454266,147.05843377 C-396.36434339,147.05843373 -396.36435761,147.05843368 -396.36458529000004,147.05843363 C-396.36435829,147.05843358 -396.36412543,147.05843352 -396.36388671000003,147.05843347 C-343.24271064,147.0457326 -61.00302123,54.328209290000004 -61.00302123,54.328209290000004 C-61.00302123,54.328209290000004 54.36842292,-476.62945279 -57.54655157,-511.5974822 C-84.89366723,-520.14213629 -115.68055714,-523.63459936 -147.75748146,-523.63725083 C-147.75684889000001,-523.6372509 -147.75684889000001,-523.63725097 -147.75748146,-523.63725104 C-147.75684889000001,-523.63725111 -147.75689632,-523.63725118 -147.75762373,-523.63725125 C-147.75842483,-523.6372513 -147.75835924,-523.63725135 -147.75742698,-523.6372514 C-147.75740817,-523.63725146 -147.75753208,-523.6372515200001 -147.7577987,-523.63725159 C-147.75584128,-523.63725167 -147.75577675,-523.63725176 -147.75760512,-523.63725184 C-147.75837949,-523.63725189 -147.75833706,-523.63725194 -147.75747781,-523.63725198 C-147.75795075,-523.63725204 -147.75793379,-523.6372520900001 -147.75742692,-523.63725215 C-147.75742922,-523.63725221 -147.76173714,-523.63725228 -147.77035068,-523.63725234 L-147.77035068,-523.63725234 Z
+2
View File
@@ -0,0 +1,2 @@
M-4.04445106,-227.53699448 C-4.04445141,-227.53699414000002 -4.04445176,-227.5369938 -4.0444521,-227.53699347 C-51.7088457,-181.43874387 -146.15069456,-90.10004021 -233.88593633,-5.24757973 C-233.88593684,-5.24757923 -233.88593736,-5.24757873 -233.88593787,-5.24757824 C-233.88593736,-5.2475777400000005 -233.88593684,-5.24757724 -233.88593632,-5.24757675 C-136.31505417,88.6780763 -38.67004972,183.1016691 -38.655462480000004,183.1157752 C-38.65546192,183.11577569 -38.65546137,183.11577618 -38.655460850000004,183.11577667 C-38.65546,183.11577618 -38.655459130000004,183.11577569 -38.65545825,183.1157752 C-38.636636960000004,183.10472910000001 63.43909693,123.19740587 172.66309485,59.09483126 C172.66309582,59.0948307 172.66309679,59.09483013 172.66309776,59.09482957 C172.66309789000002,59.094829000000004 172.66309804,59.09482844 172.66309821000002,59.09482787 C195.93027539,-28.361478 218.13974889,-146.00472757 185.99061181,-193.84229367 C185.99061146,-193.84229419000002 185.99061111,-193.84229471 185.99061077000002,-193.84229523 C185.99061077000002,-193.84229523 185.99061076,-193.84229523 185.99061076,-193.84229523 C185.99060948000002,-193.84229575 185.99060819000002,-193.84229627 185.99060690000002,-193.84229679 C163.21411887,-203.03092579 141.22123575,-211.90342815 121.23520424,-219.96631425 C121.23520325,-219.96631465000002 121.23520226000001,-219.96631505 121.23520127,-219.96631545 C121.23519826,-219.96631585 121.23519525,-219.96631625 121.23519224,-219.96631665 C81.94489447000001,-225.17543523 38.58618673,-227.27591237000001 -4.04428678,-227.53699347 C-4.0443415400000005,-227.5369938 -4.0443963,-227.53699414000002 -4.04445106,-227.53699448 L-4.04445106,-227.53699448 Z M-298.96972663,-67.82445804 C-298.96972679,-67.82445746 -298.96972695,-67.82445687 -298.96972711,-67.82445628 C-308.0266757,-34.79351542 -313.47490909,-4.88751908 -311.92312795,13.14190599 C-310.47587425,29.956875240000002 -307.39575194,46.21813067 -303.15410566,61.74459228 C-303.1541055,61.74459287 -303.15410534,61.74459345 -303.15410518,61.74459404 C-303.15410518,61.74459404 -303.15410509000003,61.74459404 -303.15410509000003,61.74459404 C-303.1541046,61.74459345 -303.15410399,61.74459287 -303.15410327,61.74459228 C-281.29005965,40.5989515 -257.83792341000003,17.91739932 -233.88593941,-5.24757675 C-233.8859389,-5.24757724 -233.88593838,-5.2475777400000005 -233.88593787,-5.24757824 C-233.88593787,-5.24757824 -233.88593787,-5.24757824 -233.88593787,-5.24757824 C-233.88593839,-5.24757873 -233.88593891,-5.24757923 -233.88593942,-5.24757973 C-255.96152711,-26.49842777 -278.03332044,-47.72378629 -298.96972479,-67.82445628 C-298.96972541,-67.82445687 -298.96972602,-67.82445746 -298.96972663,-67.82445804 L-298.96972663,-67.82445804 Z
M-4.04445105,-227.53699448 C-4.0444448,-227.53699444 -4.04443853,-227.5369944 -4.04443224,-227.53699436 C-4.04443855,-227.53699429 -4.04443619,-227.53699422 -4.04442515,-227.53699416 C-4.04443617,-227.53699411 -4.0444375500000005,-227.53699405 -4.04442929,-227.536994 C-4.04443757,-227.53699393 -4.0444355100000005,-227.53699385000002 -4.0444230800000005,-227.53699378000002 C-4.0444355100000005,-227.5369937 -4.04444445,-227.53699362 -4.0444499,-227.53699355 C-4.04444445,-227.53699362 -4.0444355100000005,-227.5369937 -4.0444230800000005,-227.53699378000002 C-4.0444355100000005,-227.53699385000002 -4.04443757,-227.53699393 -4.04442929,-227.536994 C-4.0444375500000005,-227.53699405 -4.04443617,-227.53699411 -4.04442515,-227.53699416 C-4.04443619,-227.53699422 -4.04443855,-227.53699429 -4.04443224,-227.53699436 C-4.04443853,-227.5369944 -4.0444448,-227.53699444 -4.04445105,-227.53699448 L-4.04445105,-227.53699448 Z M-4.04445201,-227.53699355 C-51.70884559,-181.43874397 -146.15069452,-90.10004025 -233.88593634,-5.24757971 C-233.88593634,-5.24757971 -233.88593825,-5.24757971 -233.88593825,-5.24757971 C-233.88593941,-5.24757971 -233.88593942,-5.24757972 -233.88593942,-5.24757972 C-255.96152637,-26.49842706 -278.03331896000003,-47.72378487 -298.9697253,-67.82445426 C-298.96972593,-67.82445486 -298.96972655,-67.82445546 -298.96972717,-67.82445606 C-298.96972733,-67.82445546 -298.96972750000003,-67.82445486 -298.96972767,-67.82445426 C-308.02667592,-34.793514200000004 -313.47490906,-4.88751871 -311.92312795,13.14190599 C-310.47587426,29.95687522 -307.39575195000003,46.21813063 -303.15410568,61.74459222 C-303.15410552000003,61.74459282 -303.15410535,61.74459342 -303.15410519,61.74459402 C-303.15410457,61.74459342 -303.15410394,61.74459282 -303.15410332,61.74459222 C-281.29005969,40.59895153 -257.83792342,17.917399330000002 -233.88593941,-5.24757675 C-233.88593941,-5.24757675 -233.88593941,-5.24757675 -233.88593941,-5.24757675 C-233.88593889,-5.24757724 -233.88593838,-5.2475777400000005 -233.88593787,-5.24757823 C-233.88593735,-5.2475777400000005 -233.88593684,-5.24757724 -233.88593632,-5.24757675 C-233.88593632,-5.24757675 -233.88593632,-5.24757675 -233.88593632,-5.24757675 C-136.31500758,88.67812116 -38.66995645,183.1017593 -38.65546246,183.11577522000002 C-38.6554624,183.11577526 -38.65546235,183.11577531 -38.6554623,183.11577536000001 C-38.65546195,183.11577567 -38.65546121,183.11577599 -38.6554601,183.1157763 C-38.65545957,183.11577599 -38.65545901,183.11577567 -38.6554584,183.11577536000001 C-38.63718132,183.10504862 63.43880567,123.19757684 172.6630948,59.094831310000004 C172.66309578,59.09483074 172.66309677,59.09483016 172.66309775,59.09482959 C172.66309789000002,59.09482901 172.66309804,59.09482844 172.66309821000002,59.09482786 C195.93027539,-28.36147801 218.13974889,-146.00472758 185.99061181,-193.84229367 C185.99061146,-193.84229419000002 185.9906111,-193.84229471 185.99061076,-193.84229523 C185.99060946,-193.84229576 185.99060816,-193.84229627 185.99060688,-193.8422968 C163.21411883,-203.03092581 141.22123568,-211.90342818 121.23520415,-219.96631429 C121.2352032,-219.96631467 121.23520226000001,-219.96631506 121.23520131000001,-219.96631544000002 C121.23519842,-219.96631582 121.23519553,-219.96631621 121.23519264000001,-219.96631659 C81.94489083,-225.17543574 38.5861782,-227.27591278 -4.04429958,-227.53699355 C-4.04429958,-227.53699355 -4.0444499,-227.53699355 -4.0444499,-227.53699355 C-4.0444499,-227.53699355 -4.04445201,-227.53699355 -4.04445201,-227.53699355 L-4.04445201,-227.53699355 Z
+2
View File
@@ -0,0 +1,2 @@
M989.66666667,768 C1058.74723866,687.80584194 1094.1623535200001,594.2965698200001 1094.1623535200001,594.2965698200001 C1173.36305304,625.10359457 1293.66666667,681 1318,768 L1299.30645729,786.39937432 L1011.11108398,786.39935303 L989.66666667,768 L989.66666667,768
M997.50296389,540.72139231 C1000.26188778,540.72139231 1002.50296389,542.96246842 1002.50296389,545.72139231 C1002.50296389,548.4803162000001 1000.26188778,550.72139231 997.50296389,550.72139231 C994.74404,550.72139231 992.50296389,548.4803162000001 992.50296389,545.72139231 C992.50296389,542.96246842 994.74404,540.72139231 997.50296389,540.72139231 L997.50296389,540.72139231 M1024.77569116,545.26684685 C1027.53461505,545.26684685 1029.77569116,547.50792297 1029.77569116,550.26684685 C1029.77569116,553.02577074 1027.53461505,555.26684685 1024.77569116,555.26684685 C1022.0167672700001,555.26684685 1019.7756911600001,553.02577074 1019.7756911600001,550.26684685 C1019.7756911600001,547.50792297 1022.0167672700001,545.26684685 1024.77569116,545.26684685 L1024.77569116,545.26684685 M1052.04841843,549.8123014 C1054.80734232,549.8123014 1057.04841843,552.05337751 1057.04841843,554.8123014 C1057.04841843,557.57122529 1054.80734232,559.8123014 1052.04841843,559.8123014 C1049.28949454,559.8123014 1047.04841843,557.57122529 1047.04841843,554.8123014 C1047.04841843,552.05337751 1049.28949454,549.8123014 1052.04841843,549.8123014 L1052.04841843,549.8123014 M1079.32114571,554.35775595 C1082.08006959,554.35775595 1084.32114571,556.5988320600001 1084.32114571,559.35775595 C1084.32114571,562.1166798300001 1082.08006959,564.35775595 1079.32114571,564.35775595 C1076.56222182,564.35775595 1074.32114571,562.1166798300001 1074.32114571,559.35775595 C1074.32114571,556.5988320600001 1076.56222182,554.35775595 1079.32114571,554.35775595 L1079.32114571,554.35775595 M1106.59387298,558.90321049 C1109.35279687,558.90321049 1111.59387298,561.1442866 1111.59387298,563.90321049 C1111.59387298,566.66213438 1109.35279687,568.90321049 1106.59387298,568.90321049 C1103.83494909,568.90321049 1101.59387298,566.66213438 1101.59387298,563.90321049 C1101.59387298,561.1442866 1103.83494909,558.90321049 1106.59387298,558.90321049 L1106.59387298,558.90321049 M1133.86660025,563.44866504 C1136.62552414,563.44866504 1138.86660025,565.68974115 1138.86660025,568.44866504 C1138.86660025,571.20758893 1136.62552414,573.44866504 1133.86660025,573.44866504 C1131.10767636,573.44866504 1128.86660025,571.20758893 1128.86660025,568.44866504 C1128.86660025,565.68974115 1131.10767636,563.44866504 1133.86660025,563.44866504 L1133.86660025,563.44866504 M1161.13932752,567.99411958 C1163.89825141,567.99411958 1166.13932752,570.2351956900001 1166.13932752,572.99411958 C1166.13932752,575.75304347 1163.89825141,577.99411958 1161.13932752,577.99411958 C1158.38040363,577.99411958 1156.13932752,575.75304347 1156.13932752,572.99411958 C1156.13932752,570.2351956900001 1158.38040363,567.99411958 1161.13932752,567.99411958 L1161.13932752,567.99411958 M1188.4120548,572.53957413 C1191.17097868,572.53957413 1193.4120548,574.78065024 1193.4120548,577.53957413 C1193.4120548,580.29849802 1191.17097868,582.53957413 1188.4120548,582.53957413 C1185.65313091,582.53957413 1183.4120548,580.29849802 1183.4120548,577.53957413 C1183.4120548,574.78065024 1185.65313091,572.53957413 1188.4120548,572.53957413 L1188.4120548,572.53957413 M1215.68478207,577.08502867 C1218.44370596,577.08502867 1220.68478207,579.32610478 1220.68478207,582.08502867 C1220.68478207,584.84395256 1218.44370596,587.08502867 1215.68478207,587.08502867 C1212.92585818,587.08502867 1210.68478207,584.84395256 1210.68478207,582.08502867 C1210.68478207,579.32610478 1212.92585818,577.08502867 1215.68478207,577.08502867 L1215.68478207,577.08502867 M1242.9575093400001,581.63048322 C1245.7164332300001,581.63048322 1247.9575093400001,583.87155933 1247.9575093400001,586.63048322 C1247.9575093400001,589.38940711 1245.7164332300001,591.63048322 1242.9575093400001,591.63048322 C1240.19858545,591.63048322 1237.9575093400001,589.38940711 1237.9575093400001,586.63048322 C1237.9575093400001,583.87155933 1240.19858545,581.63048322 1242.9575093400001,581.63048322 L1242.9575093400001,581.63048322 M1270.23023661,586.17593776 C1272.9891605,586.17593776 1275.23023661,588.41701387 1275.23023661,591.17593776 C1275.23023661,593.93486165 1272.9891605,596.17593776 1270.23023661,596.17593776 C1267.4713127300001,596.17593776 1265.23023661,593.93486165 1265.23023661,591.17593776 C1265.23023661,588.41701387 1267.4713127300001,586.17593776 1270.23023661,586.17593776 L1270.23023661,586.17593776 M1297.50296389,590.72139231 C1300.26188778,590.72139231 1302.50296389,592.96246842 1302.50296389,595.72139231 C1302.50296389,598.4803162000001 1300.26188778,600.72139231 1297.50296389,600.72139231 C1294.74404,600.72139231 1292.50296389,598.4803162000001 1292.50296389,595.72139231 C1292.50296389,592.96246842 1294.74404,590.72139231 1297.50296389,590.72139231 L1297.50296389,590.72139231 M997.50296389,569.29282088 C1000.26188778,569.29282088 1002.50296389,571.53389699 1002.50296389,574.29282088 C1002.50296389,577.05174477 1000.26188778,579.29282088 997.50296389,579.29282088 C994.74404,579.29282088 992.50296389,577.05174477 992.50296389,574.29282088 C992.50296389,571.5338969Line truncated
+2
View File
@@ -0,0 +1,2 @@
M1106.59387298,616.04606763 C1106.59302755,616.04606768 1106.59260483,616.04606774 1106.59262791,616.04606833 C1103.833007,616.04801024 1101.59387298,618.28833942 1101.59387298,621.04606763 C1101.59387298,623.80379584 1103.833007,626.04412502 1106.59028622,626.0460663700001 C1106.59036086,626.04606642 1106.59043808,626.04606647 1106.59053213,626.04606697 C1106.59044892,626.04606749 1106.59155558,626.04606756 1106.59387298,626.04606763 C1106.59619038,626.04606756 1106.59729704,626.04606749 1106.59721383,626.04606697 C1106.59730788,626.04606647 1106.5973851,626.04606642 1106.59745974,626.0460663700001 C1109.35473896,626.04412502 1111.59387298,623.80379584 1111.59387298,621.04606763 C1111.59387298,618.28833942 1109.35473896,616.04801024 1106.59511805,616.04606833 C1106.59514113,616.04606774 1106.59471841,616.04606768 1106.59387298,616.04606763 L1106.59387298,616.04606763 M1084.17786655,617.6925065400001 C1083.92379166,618.24982361 1083.66489428,618.81553061 1083.40117472,619.3894593 C1083.7593015300001,618.88489596 1084.02624396,618.3112009 1084.17786655,617.6925065400001 L1084.17786655,617.6925065400001 M1133.86660025,620.59152218 C1133.8657548200001,620.59152223 1133.8653321,620.5915222900001 1133.86535518,620.59152288 C1131.10573427,620.59346479 1128.86660025,622.83379397 1128.86660025,625.59152218 C1128.86660025,628.3492503900001 1131.10573427,630.5895795700001 1133.86301349,630.59152092 C1133.86308813,630.59152097 1133.86316535,630.59152102 1133.8632594,630.59152152 C1133.86317619,630.59152204 1133.86428286,630.59152211 1133.86660025,630.59152218 C1133.86891764,630.59152211 1133.87002431,630.59152204 1133.8699411,630.59152152 C1133.87003515,630.59152102 1133.87011237,630.59152097 1133.87018701,630.59152092 C1136.62746623,630.5895795700001 1138.86660025,628.3492503900001 1138.86660025,625.59152218 C1138.86660025,622.83379397 1136.62746623,620.59346479 1133.86784532,620.59152288 C1133.8678684000001,620.5915222900001 1133.86744568,620.59152223 1133.86660025,620.59152218 L1133.86660025,620.59152218 M1161.13932752,625.13697672 C1158.38037404,625.1370063100001 1156.13932752,627.37807105 1156.13932752,630.13697672 C1156.13932752,632.89470493 1158.37846154,635.13503411 1161.13574076,635.13697546 C1161.1358154,635.13697551 1161.13589262,635.13697556 1161.13598667,635.13697606 C1161.13590346,635.13697658 1161.1370101300001,635.1369766500001 1161.13932752,635.13697672 C1161.14164492,635.1369766500001 1161.14275158,635.13697658 1161.1426683700001,635.13697606 C1161.14276242,635.13697556 1161.14283964,635.13697551 1161.14291428,635.13697546 C1163.9001935000001,635.13503411 1166.13932752,632.89470493 1166.13932752,630.13697672 C1166.13932752,627.37805284 1163.89825141,625.13697672 1161.13932752,625.13697672 L1161.13932752,625.13697672 M1183.41754278,634.44642006 C1183.4138974100001,634.5246381100001 1183.4120548,634.6033218 1183.4120548,634.68243127 C1183.4120548,637.4413551600001 1185.65313091,639.68243127 1188.4120548,639.68243127 C1188.40973827,639.68243048 1188.4086199,639.68243011 1188.40871395,639.68243061 C1188.40863074,639.68243113 1188.40973741,639.6824312 1188.4120548,639.68243127 C1188.41437219,639.6824312 1188.4154788600001,639.68243113 1188.41539565,639.68243061 C1188.4154897,639.68243011 1188.41556692,639.68243006 1188.41564156,639.68243001 C1189.50876543,639.68166036 1190.520451,639.32907713 1191.34346184,638.73191583 C1188.7081734600001,637.28051938 1186.0650880800001,635.8520703400001 1183.41754278,634.44642006 L1183.41754278,634.44642006 M1079.32114571,640.07204166 C1079.32030028,640.07204171 1079.3198775600001,640.07204177 1079.31990064,640.0720423600001 C1076.56027973,640.07398427 1074.32114571,642.31431345 1074.32114571,645.07204166 C1074.32114571,647.8297698700001 1076.56027973,650.0700990500001 1079.31755895,650.0720404 C1079.31763359,650.07204045 1079.31771081,650.0720405 1079.31780486,650.072041 C1079.31772165,650.07204152 1079.31882832,650.07204159 1079.32114571,650.07204166 C1079.3234631,650.07204159 1079.32456977,650.07204152 1079.32448656,650.072041 C1079.32458061,650.0720405 1079.32465783,650.07204045 1079.32473247,650.0720404 C1082.08201168,650.0700990500001 1084.32114571,647.8297698700001 1084.32114571,645.07204166 C1084.32114571,642.31431345 1082.08201168,640.07398427 1079.32239078,640.0720423600001 C1079.32241386,640.07204177 1079.32199114,640.07204171 1079.32114571,640.07204166 L1079.32114571,640.07204166 M1106.59387298,644.61749621 C1106.59302755,644.61749626 1106.59260483,644.61749632 1106.59262791,644.61749691 C1103.833007,644.61943882 1101.59387298,646.859768 1101.59387298,649.61749621 C1101.59387298,652.37522441 1103.833007,654.6155536 1106.59028622,654.61749495 C1106.59036086,654.617495 1106.59043808,654.61749505 1106.59053213,654.6174955500001 C1106.59044892,654.61749607 1106.59155558,654.61749614 1106.59387298,654.61749621 C1106.59619037,654.61749614 1106.59729704,654.61749607 1106.59721383,654.6174955500001 C1106.59730788,654.61749505 1106.5973851,654.617495 1106.59745974,65Line truncated
M982.09012289,761.47344008 C1050.01995756,682.61514836 1084.81059131,590.7547421 1084.81059131,590.7547421 L1088.3841728300001,581.3191379 L1097.78750634,584.97679156 C1182.12559628,617.78212837 1302.34017279,674.88519249 1327.63040624,765.3064381 L1329.21958018,770.98827917 L1325.01476954,775.12692137 L1306.32122684,793.52629569 L1303.40221325,796.39937462 L1299.30645656,796.39937432 L1011.11108325,796.39935303 L1007.4090286,796.39935275 L1004.59941225,793.9886963 L983.15499493,775.58934327 L975.55073627,769.0648741 L982.09012289,761.47344008 L982.09012289,761.47344008 M997.24321044,774.5265599200001 L996.1783384,760.41065673 L1017.62275572,778.8100097600001 L1011.11108472,776.39935303 L1299.30645803,776.39937432 L1292.29168775,779.27245295 L1310.9852304600001,760.87307863 L1308.36959376,770.6935619 C1284.90795714,686.81017616 1163.34425414,631.9364098 1090.53720069,603.61634809 L1103.51411572,597.83839755 C1100.32064474,606.27036646 1064.3848734,696.58323987 997.24321044,774.5265599200001 L997.24321044,774.5265599200001
+2
View File
@@ -0,0 +1,2 @@
M0,340 C161.73791371000002,383.57576471 107.56418217,490.73058652000003 273,476 C419,463 481.74119799,514.69227252 481.33333333,768 C481.33333333,768 -0,768 -0,768 C-0,768 0,340 0,340 L0,340
M0.00000118,340.00000063 C0,340.01956513 0,565.33734898 0.00000305,689.0000006 C36.95467457,696.25895726 71.52028852000001,733.27936387 114.1739269,733.28721767 C114.17421003,733.2872177300001 114.17450212,733.28721778 114.17479023,733.28721818 C114.17447732000001,733.28721805 114.17457669000001,733.28721813 114.17504492,733.28721821 C114.17458128,733.28721827 114.17452308,733.28721833 114.17479023,733.28721818 C114.17447829,733.2872185900001 114.17454326000001,733.28721865 114.17493557,733.28721872 C114.17454326000001,733.28721879 114.1787321,733.28721886 114.18750207000001,733.28721892 C114.19627217,733.28721886 114.20046062,733.28721879 114.20010011000001,733.28721855 C114.20046062,733.28721865 114.20052571000001,733.2872185900001 114.20023978,733.28721811 C114.20052134000001,733.28721847 114.20047804000001,733.28721843 114.20010011000001,733.28721855 C114.20048076,733.28721833 114.20042242,733.28721827 114.19995778,733.28721821 C114.20042726,733.28721813 114.20052685,733.28721805 114.20023978,733.28721811 C114.20050169,733.28721778 114.20079386,733.2872177300001 114.20107659,733.28721767 C136.17620277,733.2831713100001 160.2983806,723.4526911 188,694.66666667 C280.96421132,598.06328693 341.25735896000003,565.3752631 404.56938754000004,565.36776692 C404.56967787,565.36776666 404.56967196,565.36776662 404.56930161,565.36776624 C404.56893409,565.36776651 404.56909019,565.36776645 404.56977678,565.36776639 C404.56777842,565.36776631 404.56762556,565.36776623 404.56930161,565.36776624 C404.56933179000004,565.36776595 404.57194292,565.3677659 404.57711156,565.3677658400001 C404.58223904,565.3677659 404.58480747,565.36776595 404.58478914,565.36776636 C404.5865175,565.36776623 404.58635002,565.36776631 404.58428263,565.36776639 C404.58503745,565.36776645 404.58520212,565.36776651 404.58478914,565.36776636 C404.58565083,565.36776678 404.58561061,565.3677668500001 404.58465731,565.36776702 C422.0448013,565.36982731 439.71964088,567.85379516 458.37025448,572.16574812 C432.70315584,498.6798599 384.8001224,474.16753872 311.04913551,474.16125007 C311.05037295,474.16124990000003 311.05042416000003,474.16124984 311.04930474,474.16124944 C311.04983879,474.16124959 311.04962484,474.16124953 311.04864823,474.16124947000003 C311.05133979,474.16124939 311.05155656,474.16124932 311.04930474,474.16124944 C311.04932269,474.16124906 311.0459606,474.16124901 311.03925496,474.16124896 C311.03256688,474.16124901 311.02922253,474.16124906 311.02925759,474.16124944 C311.02698744,474.16124932 311.02721009,474.16124939 311.02992915,474.16124947000003 C311.02892511,474.16124953 311.02870773,474.16124959 311.02925759,474.16124944 C311.02811977,474.16124984 311.02817483,474.16124990000003 311.02943543,474.16125007 C299.02774707000003,474.16227143000003 286.36259278,474.81018009 273,476 C262.50555953,476.93443648 252.89483097000002,477.37840436 244.05664154000002,477.37924601000003 C244.0561175,477.37924606 244.0555754,477.37924611 244.05501524000002,477.37924616000004 C244.05557329,477.3792462 244.05560823000002,477.37924625 244.05512578,477.37924656 C244.05562249000002,477.37924637000003 244.05543748,477.37924644000003 244.05456503,477.37924652000004 C244.05542822,477.37924658000003 244.05553661000002,477.37924663 244.05489021,477.37924669 C244.0555314,477.37924673000003 244.05561182,477.37924678 244.05512578,477.37924656 C244.0556202,477.37924689 244.05549928,477.37924695000004 244.05476873,477.37924701000003 C244.05549928,477.37924708 244.04773046,477.37924714 244.03146226,477.37924721 C244.0152888,477.37924714 244.00743904,477.37924708 244.00787253000001,477.37924685 C244.00743904,477.37924695000004 244.00735431,477.37924689 244.00769022,477.37924642 C244.00729702,477.37924678 244.00735477,477.37924673000003 244.00787253000001,477.37924685 C244.00739042,477.37924663 244.0074595,477.37924658000003 244.00803933,477.37924652000004 C244.00752289000002,477.37924644000003 244.00739892,477.37924637000003 244.00769022,477.37924642 C244.00734149000002,477.37924611 244.01524745,477.37924646 244.03146226,477.37924721 C113.5732624,477.37924721 151.46830545,380.80890773 0.00000118,340.00000063 L0.00000118,340.00000063
+2
View File
@@ -0,0 +1,2 @@
M-309.57295624,-270.00777117 C-309.57295624,-270.00777117 -311.41340739000003,-216.28078669 -311.41340739000003,-216.28078669 C-311.41340739000003,-216.28078669 -351.51886288000003,-252.07923027 -351.51886288000003,-252.07923027 C-351.51886288000003,-252.07923027 -329.86578716,-202.87434805 -329.86578716,-202.87434805 C-329.86578716,-202.87434805 -381.53191653,-217.72652625 -381.53191653,-217.72652625 C-381.53191653,-217.72652625 -340.67397113,-182.78937667 -340.67397113,-182.78937667 C-340.67397113,-182.78937667 -393.66766383,-173.75363345 -393.66766383,-173.75363345 C-393.66766383,-173.75363345 -341.69727067,-160.00394875 -341.69726593,-160.00394749 C-341.69726593,-160.00394749 -341.69726593,-160.00394749 -341.69726593,-160.00394749 C-341.69726746000003,-160.0039464 -385.52247121,-128.8699226 -385.52247121,-128.8699226 C-385.52247121,-128.8699226 -358.81363105,-134.0109177 -343.39203698,-136.97931091 C-343.97299745,-132.31826018 -345.06350128,-123.56913893000001 -346.17106274,-114.68316414 C-350.49807163,-106.84325123 -354.75843994,-99.12408138 -357.02812992,-95.01173213 C-367.75968386,-106.47779825 -386.34578834,-126.33601300000001 -386.34578834,-126.33601300000001 C-386.34578834,-126.33601300000001 -386.34578834,-126.33601300000001 -386.34578834,-126.33601300000001 C-386.34578834,-126.33601300000001 -369.19057317,-75.38824843 -369.19057317,-75.38824843 C-369.19057317,-75.38824843 -419.31737091,-94.81195225 -419.31737091,-94.81195225 C-419.31737091,-94.81195225 -381.75564952,-56.35297139 -381.75564952,-56.35297139 C-381.75564952,-56.35297139 -435.34596299000003,-52.103921400000004 -435.34596299000003,-52.103921400000004 C-435.34596299000003,-52.103921400000004 -384.81729498,-33.75099708 -384.81729498,-33.75099708 C-384.81729498,-33.75099708 -431.25690533,-6.67076973 -431.25690533,-6.67076973 C-431.25690533,-6.67076973 -377.76911308,-12.05892369 -377.76911308,-12.05892369 C-377.76911308,-12.05892369 -407.85264390000003,32.47789237 -407.86008457,32.48890782 C-407.86008503,32.4889085 -407.86008549,32.48890918 -407.86008595,32.48890986 C-407.86008484,32.48890918 -407.86008372000003,32.4889085 -407.86008261,32.48890782 C-407.84579985,32.480166770000004 -362.02000608000003,4.4347767000000005 -362.00708544,4.42686927 C-362.00708453,4.42686871 -362.00708362,4.42686816 -362.00708271,4.4268676000000005 C-362.00708279,4.42686816 -362.00708287000003,4.42686871 -362.00708295,4.42686927 C-362.00867591,4.43775696 -369.78739577,57.60446257 -369.78953143,57.619059570000005 C-369.78953158,57.61906058 -369.78953172,57.61906158 -369.78953187,57.61906258 C-369.78953122,57.61906158 -369.78953058,57.61906058 -369.78952993,57.619059570000005 C-369.7808544,57.60560762 -340.66022242,12.45225812 -340.6530686,12.44116567 C-340.65306816000003,12.44116499 -340.65306772,12.44116431 -340.65306728,12.44116363 C-340.65306707,12.44116431 -340.65306685,12.44116499 -340.65306664,12.44116567 C-340.64936448000003,12.45298614 -324.59027851,63.727362150000005 -324.58558427,63.74235018 C-324.58558393,63.74235128 -324.58558358,63.74235237 -324.58558324,63.742353470000005 C-324.5855831,63.74235237 -324.58558297,63.74235128 -324.58558283,63.74235018 C-324.58422772,63.73147814 -321.22108328,36.74894232 -319.27904968,21.16799066 C-315.02565368,23.160860800000002 -307.04172911,26.901618720000002 -298.9329202,30.70088949 C-294.32976908,35.619099410000004 -289.7799484,40.480328730000004 -286.44175296000003,44.04700415 C-286.44175296000003,44.04700415 -286.44175296000003,44.04700415 -286.44175296000003,44.04700415 C-290.93120416,42.75644629 -293.88219469,41.90814127 -293.88662067,41.90686895 C-293.88662234000003,41.90686848 -293.886624,41.906868 -293.88662566,41.90686752 C-293.8866251,41.906868 -293.88662454,41.90686848 -293.88662398,41.90686895 C-293.88399641,41.90911576 -290.79214413,44.55292235 -286.31055615,48.385075640000004 C-286.31055615,48.385075640000004 -286.31055615,48.385075640000004 -286.31055615,48.385075640000004 C-300.73158259,55.14184901 -322.42221072,65.30469416 -322.43010884,65.30839472 C-322.43010956,65.30839506 -322.43011028,65.30839539 -322.430111,65.30839573 C-322.43007932,65.30839539 -322.43004765,65.30839506 -322.43001597,65.30839472 C-322.34765695,65.30751858 -268.75710222,64.73741871 -268.67474949,64.73654264 C-268.67471782,64.7365423 -268.67468615,64.73654197 -268.67465448,64.73654163 C-268.67465475,64.73654197 -268.67465503,64.7365423 -268.6746553,64.73654264 C-268.6773476,64.73984596 -275.27893096,72.83969197 -282.66038186000003,81.89640037 C-294.78545077,83.96379752 -306.02237297,85.87976032 -306.02237297,85.87976032 C-306.02237297,85.87976032 -298.65872262,87.82794398 -289.47501971,90.25765466 C-289.47501971,90.25765466 -289.47501971,90.25765466 -289.47501971,90.25765466 C-296.51657572,98.89732731000001 -302.63423901,106.40342515 -302.63772546,106.40770286 C-302.63772596,106.40770347 -302.63772645,106.40770408 -302.63772695,106.40770469 C-302.63772572,106.40770408 -302.63772449,106.40770347 -302.63772325,106.40770286 C-30Line truncated
M324.51529217,288.9978096 C324.51529217,288.9978096 294.4243193,244.44997606 294.4243193,244.44997606 C294.4243193,244.44997606 347.91211155,249.83813001000001 347.91211155,249.83813001000001 C347.91211155,249.83813001000001 301.47250119,222.75790267 301.47250119,222.75790267 C301.47250119,222.75790267 352.0011692,204.40497835 352.0011692,204.40497835 C352.0011692,204.40497835 298.41085574,200.15592835 298.41085574,200.15592835 C298.41085574,200.15592835 335.97257713,161.69694748 335.97257713,161.69694748 C335.97257713,161.69694748 285.84577938,181.12065131 285.84577938,181.12065131 C285.84577938,181.12065131 303.00099455000003,130.17288675 303.00099455000003,130.17288675 C303.00099455000003,130.17288675 284.41489007,150.0311015 273.68333613,161.49716761 C271.41364616,157.38481836 267.15327784,149.66564851 262.82626895,141.82573561 C261.71870749,132.93976082 260.62820367,124.19063956000001 260.04724319,119.52958884 C275.46883726,122.49798204 302.17767742,127.63897715 302.17767742,127.63897715 C302.17767742,127.63897715 258.35247215,96.50495226 258.35247215,96.50495226 C258.35247215,96.50495226 310.32287005,82.7552663 310.32287005,82.7552663 C310.32287005,82.7552663 257.32917734,73.71952306 257.32917734,73.71952306 C257.32917734,73.71952306 298.18712275,38.78237349 298.18712275,38.78237349 C298.18712275,38.78237349 246.52099336,53.6345517 246.52099336,53.6345517 C246.52099336,53.6345517 268.17406909,4.42966947 268.17406909,4.42966947 C268.17406909,4.42966947 228.06861361,40.22811306 228.06861361,40.22811306 C228.06861361,40.22811306 226.22816245,-13.49887143 226.22816245,-13.49887143 C226.22816245,-13.49887143 205.62675818,36.15551674 205.62675818,36.15551674 C205.62675818,36.15551674 180.65730406,-11.45228181 180.65730406,-11.45228181 C180.65730406,-11.45228181 183.64031178,42.2233904 183.64031178,42.2233904 C183.64031178,42.2233904 140.48736132,10.16408614 140.48736132,10.16408614 C140.48736132,10.16408614 153.63022372,33.97704776 161.21886807,47.72657135 C156.60641874,48.61438899 147.94852575000002,50.28088537 139.15520621000002,51.97344905 C130.36188667,50.28088537 121.70399368,48.61438899 117.09154435,47.72657135 C124.67889191,33.97939736 137.8185594,10.17222446 137.82302529,10.16410572 C137.75685999,10.213261020000001 94.73628666,42.17421931 94.67013882,42.22336141 C94.67063702,42.21373905 97.65238679000001,-11.43929805 97.65310768,-11.45227981 C97.64512328,-11.43705714 72.69103151,36.14145096 72.68365476,36.15551444 C72.67954399,36.14561 52.08780283,-13.485487710000001 52.08225031,-13.49886932 C52.081699040000004,-13.48278844 50.2422607,40.21462969 50.2417979,40.22811126 C50.23275837,40.22004347 10.14795424,4.44003347 10.13634463,4.42967126 C10.14261211,4.44391475 31.78453808,53.62346007 31.78941737,53.63455046 C31.77645289,53.63082438 -19.86124424,38.78681945 -19.87670788,38.78237525 C-19.865697570000002,38.79179037 20.97308336,73.71255262 20.98123284,73.71952276 C20.96557803,73.72219269 -31.99551057,82.75237671000001 -32.01245485,82.75526654000001 C-31.99988635,82.75859224 19.94859556,96.50247995000001 19.957913350000002,96.50497058 C19.8907232,96.55270420000001 -23.80004272,127.59122153 -23.86722549,127.6389497 C-23.85813697,127.63722016 2.84421051,122.49747479 18.26316924,119.52958884 C17.68220876,124.19063957 16.59170494,132.93976082 15.48414348,141.8257356 C11.15713459,149.6656485 6.8967662700000005,157.38481836 4.62707629,161.49716761 C-6.10447764,150.03110149 -24.69058213,130.17288674 -24.69058213,130.17288674 C-24.69058213,130.17288674 -7.53536695,181.12065132 -7.53536695,181.12065132 C-7.53536695,181.12065132 -57.662164690000004,161.69694749 -57.662164690000004,161.69694749 C-57.662164690000004,161.69694749 -20.10044332,200.15592835 -20.10044332,200.15592835 C-20.10044332,200.15592835 -73.69075677000001,204.40497834 -73.69075677000001,204.40497834 C-73.69075677000001,204.40497834 -23.16208876,222.75790267 -23.16208876,222.75790267 C-23.16208876,222.75790267 -69.60169912,249.83813001000001 -69.60169912,249.83813001000001 C-69.60169912,249.83813001000001 -16.11390686,244.44997605 -16.11390686,244.44997605 C-16.11390686,244.44997605 -46.20487974,288.9978096 -46.20487974,288.9978096 C-46.20487974,288.9978096 -0.3518765,260.93576734 -0.3518765,260.93576734 C-0.3518765,260.93576734 -8.13432566,314.12796233 -8.13432566,314.12796233 C-8.13432566,314.12796233 21.00213893,268.95006338 21.00213893,268.95006338 C21.00213893,268.95006338 37.06962297,320.25125322 37.06962297,320.25125322 C37.06962297,320.25125322 40.43373158,293.26098189 42.37615653,277.67689041 C46.62955254,279.66976054 54.613477100000004,283.41051847 62.72228602,287.20978924 C68.84136699,293.74768556 74.86620757,300.18489127 78.07591135,303.61428052 C63.85480191,310.27738553 39.22509522,321.81729547000003 39.22509522,321.81729547000003 C39.22509522,321.81729547000003 92.98055174,321.24544137 92.98055174,321.24544137 C92.98055174,321.24544137 59.01747926,362.91660443 59.01747926,362.91660443 C59.01747926,362.91660443 107.20135387,339.07Line truncated
+2
View File
@@ -0,0 +1,2 @@
M0,-111.5 C-2.03969458,-24.64993384 -0.75486715,-23.863645860000002 0.38532272,-22.922613090000002 C1.527117,-21.98353102 2.54469833,-20.872877210000002 3.39448531,-19.60851345 L0,-111.5
M-17.01537334,2.69695098 L-1.99367877,-24.5346243 C5.5261131,-20.38649788 8.259388,-10.92778124 4.11126158,-3.40798937 L-17.01537334,2.69695098
+2
View File
@@ -0,0 +1,2 @@
M6025,9840.5 C6025,9811.9819969761829 6004.7190026423077,9772.5 5926,9772.5 C5848.2352234822274,9772.5 5830,9811.2112664255637 5830,9830.5 C5830,9850.7486624496178 5841.3744295221686,9878.5670833890726 5926.9050692843539,9897.4042733062724 C5923.1414656954139,9896.5753806921766 5920.6158532561158,9896.6881708042274 5919.7428657446362,9896.7806765477999 C5922.2781984304556,9896.5120211177727 5929.7581421036521,9894.1586192843624 5943.034773076467,9893.7664964749565 C5964.0285023729066,9893.1464506117791 5992.0966123945063,9900.6639968701948 6009.9213151129179,9929.2664268136887 C6020.7494209612541,9946.6417594540435 6036.6421514647836,9954.2087579702147 6069.7403859423248,9970.9765860094976 C6096.4064338250982,9984.4858173157481 6154,10013.416342020173 6154,10086.5 C6154,10204.291870660425 6051.8757061959668,10272.5 5926,10272.5 C5793.4238484174057,10272.5 5699,10181.937205348144 5699,10073.5 C5699,10073.5 5819,10073.5 5819,10073.5 C5819,10105.062794651856 5848.5761515825943,10152.5 5926,10152.5 C6014.1242938040332,10152.5 6034,10112.708129339575 6034,10086.5 C6034,10086.04891236476 6033.9807017554267,10085.850609014404 6033.9729096025885,10085.785708608404 C6034.1402097365763,10087.179142085055 6035.2683900531028,10089.422252966504 6036.0674605371369,10090.290908271427 C6031.784136836327,10085.634583291656 6025.5593821736265,10083.613798019922 5993.9139736805519,10067.1130608739 C5968.463992389773,10053.842780234541 5933.1526880433003,10032.968601506755 5908.0786848870821,9992.7335731863113 C5922.5320746617581,10015.926221894515 5948.1042351958995,10013.643209438689 5948.6368267411644,10013.571880444044 C5943.041688160647,10014.321226943064 5926.1957043674656,10020.123898138048 5901.0949307156452,10014.595726693728 C5804.5118859197682,9993.3243651949706 5710,9942.251337550375 5710,9830.5 C5710,9721.7887335744363 5807.7647765177726,9652.5 5926,9652.5 C6055.2809973576923,9652.5 6145,9731.0180030238171 6145,9840.5 C6145,9840.5 6025,9840.5 6025,9840.5 Z
M6025,9840.5 C6025,9811.9819969761829 6004.7190026423077,9772.5 5926,9772.5 C5926,9772.5 5926,9652.5 5926,9652.5 C6055.2809973576923,9652.5 6145,9731.0180030238171 6145,9840.5 C6145,9840.5 6025,9840.5 6025,9840.5 Z M5926,9772.5 C5848.2352234822274,9772.5 5830,9811.2112664255637 5830,9830.5 C5830,9830.5 5710,9830.5 5710,9830.5 C5710,9721.7887335744363 5807.7647765177726,9652.5 5926,9652.5 C5926,9652.5 5926,9772.5 5926,9772.5 Z M5830,9830.5 C5830,9850.7486624496178 5841.3744295221686,9878.5670833890726 5926.9050692843539,9897.4042733062724 C5926.9050692843539,9897.4042733062724 5901.0949307156461,10014.595726693728 5901.0949307156461,10014.595726693728 C5804.5118859197692,9993.3243651949706 5710,9942.251337550375 5710,9830.5 C5710,9830.5 5830,9830.5 5830,9830.5 Z M5926.9050692843502,9897.4042733062724 C5923.1414656954148,9896.5753806921766 5920.6158532561158,9896.6881708042274 5919.7428657446362,9896.7806765477999 C5919.7428657446362,9896.7806765477999 5932.3878200776235,10016.112588163747 5932.3878200776235,10016.112588163747 C5924.6825868815522,10016.929069862314 5913.6510111797834,10017.361066377565 5901.0949307156498,10014.595726693728 C5901.0949307156498,10014.595726693728 5926.9050692843502,9897.4042733062724 5926.9050692843502,9897.4042733062724 Z M5919.7428657446435,9896.7806765477981 C5922.2781984304638,9896.5120211177709 5929.7581421036521,9894.1586192843624 5943.034773076467,9893.7664964749565 C5964.0285023729066,9893.1464506117791 5992.0966123945063,9900.6639968701948 6009.9213151129161,9929.2664268136868 C6009.9213151129161,9929.2664268136868 5908.0786848870839,9992.7335731863132 5908.0786848870839,9992.7335731863132 C5922.5320813616336,10015.926232645474 5948.1042554148153,10013.643206799046 5948.6368267411754,10013.571880444044 C5946.7576348925104,10013.823557352434 5941.1787512170895,10015.181060952815 5932.3878200776162,10016.112588163749 C5932.3878200776162,10016.112588163749 5919.7428657446435,9896.7806765477981 5919.7428657446435,9896.7806765477981 Z M5919.7428657446389,9896.7806765477981 C5952.695463221462,9893.2888688275925 5982.2394909988989,9917.1715577124596 5985.7312987191053,9950.1241551892817 C5989.2231064393118,9983.0767526661039 5965.3404175544438,10012.620780443543 5932.3878200776207,10016.112588163749 C5899.4352226007977,10019.604395883955 5869.8911948233608,9995.7217069990875 5866.3993871031544,9962.7691095222654 C5862.9075793829479,9929.8165120454432 5886.7902682678159,9900.2724842680036 5919.7428657446389,9896.7806765477981 Z M6009.9213151129179,9929.2664268136887 C6020.7494209612541,9946.6417594540435 6036.6421514647836,9954.2087579702147 6069.7403859423248,9970.9765860094976 C6096.4064338250982,9984.4858173157481 6154,10013.416342020173 6154,10086.5 C6154,10086.5 6034,10086.5 6034,10086.5 C6034,10086.04891236476 6033.9807017554267,10085.850609014404 6033.9729096025885,10085.785708608404 C6034.1402097365763,10087.179142085055 6035.2683900531028,10089.422252966504 6036.0674605371369,10090.290908271427 C6031.784136836327,10085.634583291656 6025.5593821736265,10083.613798019922 5993.9139736805519,10067.1130608739 C5968.463992389773,10053.842780234541 5933.1526880433003,10032.968601506755 5908.0786848870821,9992.7335731863113 C5908.0786848870821,9992.7335731863113 6009.9213151129179,9929.2664268136887 6009.9213151129179,9929.2664268136887 Z M6154,10086.5 C6154,10204.291870660425 6051.8757061959668,10272.5 5926,10272.5 C5926,10272.5 5926,10152.5 5926,10152.5 C6014.1242938040332,10152.5 6034,10112.708129339575 6034,10086.5 C6034,10086.5 6154,10086.5 6154,10086.5 Z M5926,10272.5 C5793.4238484174057,10272.5 5699,10181.937205348144 5699,10073.5 C5699,10073.5 5819,10073.5 5819,10073.5 C5819,10105.062794651856 5848.5761515825943,10152.5 5926,10152.5 C5926,10152.5 5926,10272.5 5926,10272.5 Z M5926.9050692843539,9897.4042733062724 C5959.2665955425527,9904.5315462675317 5979.7229996549859,9936.543543026155 5972.5957266937276,9968.9050692843539 C5965.4684537324692,10001.266595542553 5933.456456973845,10021.722999654987 5901.0949307156461,10014.595726693728 C5868.7334044574473,10007.468453732468 5848.2770003450141,9975.456456973845 5855.4042733062724,9943.0949307156461 C5862.5315462675308,9910.7334044574473 5894.543543026155,9890.2770003450132 5926.9050692843539,9897.4042733062724 Z M6009.9213151129179,9929.2664268136887 C6027.447283641357,9957.3894925918812 6018.8566389645039,9994.3953465844788 5990.7335731863113,10011.921315112919 C5962.6105074081188,10029.447283641359 5925.6046534155212,10020.856638964504 5908.0786848870821,9992.7335731863113 C5890.552716358643,9964.6105074081188 5899.1433610354961,9927.6046534155212 5927.2664268136887,9910.0786848870812 C5955.3894925918812,9892.5527163586412 5992.3953465844788,9901.1433610354961 6009.9213151129179,9929.2664268136887 Z
+567
View File
@@ -0,0 +1,567 @@
use kurbo::{Affine, BezPath, ParamCurve as _, Point, Rect, Vec2};
use libtest_mimic::{Arguments, Failed, Trial};
use linesweeper::{
Segment, Segments,
sweep::{SweepLineBuffers, SweepLineRange, SweepLineRangeBuffers, Sweeper, Correctness},
topology::Topology,
};
use std::{
collections::HashMap,
path::{Path, PathBuf},
};
use tiny_skia::Pixmap;
fn main() {
let args = Arguments::from_args();
let mut tests = sweep_snapshot_diffs();
tests.extend(position_snapshot_diffs());
tests.extend(output_snapshot_diffs());
libtest_mimic::run(&args, tests).exit();
}
fn path_color(idx: usize) -> tiny_skia::Color {
let palette = [
tiny_skia::Color::from_rgba8(0x00, 0x5F, 0x73, 0xFF),
tiny_skia::Color::from_rgba8(0x94, 0xD2, 0xBD, 0xFF),
tiny_skia::Color::from_rgba8(0xE9, 0xD8, 0xA6, 0xFF),
tiny_skia::Color::from_rgba8(0xEE, 0x9B, 0x00, 0xFF),
tiny_skia::Color::from_rgba8(0xCA, 0x67, 0x02, 0xFF),
tiny_skia::Color::from_rgba8(0xBB, 0x3E, 0x03, 0xFF),
tiny_skia::Color::from_rgba8(0xAE, 0x20, 0x12, 0xFF),
];
palette[idx % palette.len()]
}
fn sweep_line_color() -> tiny_skia::Color {
tiny_skia::Color::from_rgba8(0x9B, 0x22, 0x26, 0xFF)
}
fn sweep_snapshot_diffs() -> Vec<Trial> {
let ws = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let paths = glob::glob(&format!("{ws}/tests/snapshots/inputs/sweep/**/*.svg")).unwrap();
paths
.into_iter()
.map(|p| {
let p = p.unwrap();
let name = input_path_base(&p).display().to_string();
Trial::test(name, || generate_sweep_snapshot(p))
})
.collect()
}
fn position_snapshot_diffs() -> Vec<Trial> {
let ws = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let paths = glob::glob(&format!("{ws}/tests/snapshots/inputs/position/**/*.svg")).unwrap();
paths
.into_iter()
.map(|p| {
let p = p.unwrap();
let name = input_path_base(&p).display().to_string();
Trial::test(name, || {
generate_position_snapshot(p.clone(), false).and(generate_position_snapshot(p, true))
})
})
.collect()
}
fn output_snapshot_diffs() -> Vec<Trial> {
let ws = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let paths = glob::glob(&format!("{ws}/tests/snapshots/inputs/output/**/*.svg")).unwrap();
paths
.into_iter()
.map(|p| {
let p = p.unwrap();
let name = input_path_base(&p).display().to_string();
Trial::test(name, || {
generate_output_snapshot(p.clone(), false).and(generate_output_snapshot(p, true))
})
})
.collect()
}
fn input_path_base(input_path: &Path) -> &Path {
let ws = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let base = format!("{ws}/tests/snapshots/inputs");
input_path.strip_prefix(base).unwrap()
}
fn output_path_for(input_path: &Path, correct: bool) -> PathBuf {
let mut ws: PathBuf = std::env::var_os("CARGO_MANIFEST_DIR").unwrap().into();
ws.push("target/snapshots/snapshots");
if correct {
ws.push("correct");
}
ws.push(input_path);
ws.set_extension("png");
ws
}
fn saved_snapshot_path_for(input_path: &Path, correct: bool) -> PathBuf {
let mut ws: PathBuf = std::env::var_os("CARGO_MANIFEST_DIR").unwrap().into();
ws.push("tests/snapshots/snapshots");
if correct {
ws.push("correct");
}
ws.push(input_path);
ws.set_extension("png");
ws
}
fn skia_path(elts: impl IntoIterator<Item = kurbo::PathEl>) -> tiny_skia::Path {
let mut pb = tiny_skia::PathBuilder::new();
for elt in elts {
match elt {
kurbo::PathEl::MoveTo(p) => pb.move_to(p.x as f32, p.y as f32),
kurbo::PathEl::LineTo(p) => pb.line_to(p.x as f32, p.y as f32),
kurbo::PathEl::QuadTo(p0, p1) => {
pb.quad_to(p0.x as f32, p0.y as f32, p1.x as f32, p1.y as f32)
}
kurbo::PathEl::CurveTo(p0, p1, p2) => pb.cubic_to(
p0.x as f32,
p0.y as f32,
p1.x as f32,
p1.y as f32,
p2.x as f32,
p2.y as f32,
),
kurbo::PathEl::ClosePath => pb.close(),
}
}
pb.finish().unwrap()
}
fn skia_kurbo_seg(seg: kurbo::PathSeg) -> tiny_skia::Path {
let mut pb = tiny_skia::PathBuilder::new();
let start = seg.start();
pb.move_to(start.x as f32, start.y as f32);
match seg {
kurbo::PathSeg::Line(ell) => pb.line_to(ell.p1.x as f32, ell.p1.y as f32),
kurbo::PathSeg::Quad(q) => {
pb.quad_to(q.p1.x as f32, q.p1.y as f32, q.p2.x as f32, q.p2.y as f32)
}
kurbo::PathSeg::Cubic(c) => pb.cubic_to(
c.p1.x as f32,
c.p1.y as f32,
c.p2.x as f32,
c.p2.y as f32,
c.p3.x as f32,
c.p3.y as f32,
),
}
pb.finish().unwrap()
}
fn skia_cubic(s: &kurbo::CubicBez) -> tiny_skia::Path {
let mut pb = tiny_skia::PathBuilder::new();
pb.move_to(s.p0.x as f32, s.p0.y as f32);
pb.cubic_to(
s.p1.x as f32,
s.p1.y as f32,
s.p2.x as f32,
s.p2.y as f32,
s.p3.x as f32,
s.p3.y as f32,
);
pb.finish().unwrap()
}
fn skia_segment(s: &Segment) -> tiny_skia::Path {
skia_cubic(&s.to_kurbo_cubic())
}
fn line(p: impl Into<Point>, q: impl Into<Point>) -> tiny_skia::Path {
let mut pb = tiny_skia::PathBuilder::new();
let p = p.into();
pb.move_to(p.x as f32, p.y as f32);
let q = q.into();
pb.line_to(q.x as f32, q.y as f32);
pb.finish().unwrap()
}
fn two_lines(p: impl Into<Point>, q: impl Into<Point>, r: impl Into<Point>) -> tiny_skia::Path {
let mut pb = tiny_skia::PathBuilder::new();
let p = p.into();
pb.move_to(p.x as f32, p.y as f32);
let q = q.into();
pb.line_to(q.x as f32, q.y as f32);
let r = r.into();
pb.line_to(r.x as f32, r.y as f32);
pb.finish().unwrap()
}
fn draw_orig_path(pixmap: &mut Pixmap, path: &BezPath, offset: kurbo::Point) {
let p = skia_path(path);
let mut paint = tiny_skia::Paint::default();
paint.set_color_rgba8(0, 0, 0, 255);
let stroke = tiny_skia::Stroke {
width: 1.0,
..Default::default()
};
let transform = tiny_skia::Transform::from_translate(offset.x as f32, offset.y as f32);
pixmap.stroke_path(&p, &paint, &stroke, transform, None);
}
fn adjust_x_positions(orig: &[f64], padding: f64, min_x: f64, max_x: f64) -> Vec<f64> {
if orig.is_empty() {
return Vec::new();
}
let mut padded = Vec::with_capacity(orig.len());
let mut max_so_far = f64::NEG_INFINITY;
for &x in orig {
let x = x.max(max_so_far + padding);
padded.push(x);
max_so_far = x;
}
let orig_first = *orig.first().unwrap();
let orig_last = *orig.last().unwrap();
let padded_min = *padded.first().unwrap();
let padded_max = *padded.last().unwrap();
let mut mid_shift = (orig_first + orig_last - padded_min - padded_max) / 2.0;
mid_shift = mid_shift.clamp(min_x - padded_min, max_x - padded_max);
if padded_max - padded_min > max_x - min_x {
mid_shift = (max_x + min_x - padded_max - padded_min) / 2.0;
}
for x in &mut padded {
*x += mid_shift;
}
padded
}
fn color(c: tiny_skia::Color) -> tiny_skia::Paint<'static> {
let mut p = tiny_skia::Paint::default();
p.set_color(c);
p
}
fn draw_sweep_line_range(
pixmap: &mut Pixmap,
segments: &Segments,
range: SweepLineRange<'_, '_, '_>,
bbox: kurbo::Rect,
padding: f64,
) {
let y = range.line().y();
let mut paint = tiny_skia::Paint::default();
paint.set_color(sweep_line_color());
let stroke = tiny_skia::Stroke {
width: 2.0,
..Default::default()
};
let thick_stroke = tiny_skia::Stroke {
width: 4.0,
..Default::default()
};
let s_line = line(
(bbox.min_x() - padding, bbox.min_y() + y),
(bbox.max_x() + padding, bbox.min_y() + y),
);
pixmap.stroke_path(
&s_line,
&paint,
&stroke,
tiny_skia::Transform::identity(),
None,
);
let old_segs: Vec<_> = range.old_segment_range().collect();
let new_segs: Vec<_> = range.segment_range().collect();
let really_new_segs = new_segs.iter().filter(|s| !old_segs.contains(s));
let seg_color: HashMap<_, _> = old_segs
.iter()
.chain(really_new_segs.clone())
.enumerate()
.map(|(color_idx, seg_idx)| (seg_idx, color_idx))
.collect();
let origin = bbox.origin();
for seg_idx in old_segs.iter().chain(really_new_segs) {
let color_idx = seg_color[seg_idx];
let seg = &segments[*seg_idx];
let p = skia_segment(seg);
let mut paint = tiny_skia::Paint::default();
paint.set_color(path_color(color_idx));
let transform = tiny_skia::Transform::from_translate(origin.x as f32, origin.y as f32);
pixmap.stroke_path(&p, &paint, &stroke, transform, None);
}
let old_seg_positions: Vec<_> = old_segs.iter().map(|s| segments[*s].at_y(y)).collect();
let padded_old_seg_positions: Vec<_> = adjust_x_positions(
&old_seg_positions,
padding / 2.0,
bbox.min_x() - padding,
bbox.max_x() + padding,
);
for ((&px, &x), seg_idx) in padded_old_seg_positions
.iter()
.zip(&old_seg_positions)
.zip(&old_segs)
{
let p = two_lines(
(bbox.min_x() + px, bbox.min_y()),
(bbox.min_x() + px, bbox.min_y() + y - padding / 2.0),
(bbox.min_x() + x, bbox.min_y() + y),
);
let color_idx = seg_color[seg_idx];
let c = path_color(color_idx);
pixmap.stroke_path(
&p,
&color(c),
&thick_stroke,
tiny_skia::Transform::identity(),
None,
);
}
let new_seg_positions: Vec<_> = new_segs.iter().map(|s| segments[*s].at_y(y)).collect();
let padded_new_seg_positions: Vec<_> = adjust_x_positions(
&new_seg_positions,
padding / 2.0,
bbox.min_x() - padding,
bbox.max_x() + padding,
);
for ((&px, &x), seg_idx) in padded_new_seg_positions
.iter()
.zip(&new_seg_positions)
.zip(&new_segs)
{
let p = two_lines(
(bbox.min_x() + x, bbox.min_y() + y),
(bbox.min_x() + px, bbox.min_y() + y + padding / 2.0),
(bbox.min_x() + px, bbox.max_y()),
);
let color_idx = seg_color[seg_idx];
let c = path_color(color_idx);
pixmap.stroke_path(
&p,
&color(c),
&thick_stroke,
tiny_skia::Transform::identity(),
None,
);
}
}
fn generate_sweep_snapshot(path: PathBuf) -> Result<(), Failed> {
let input = std::fs::read_to_string(&path).unwrap();
let tree = usvg::Tree::from_str(&input, &usvg::Options::default()).unwrap();
let bezs = linesweeper_util::svg_to_bezpaths(&tree);
let bbox = linesweeper_util::bezier_bounding_box(bezs.iter());
let bez: BezPath = bezs
.into_iter()
.flat_map(|p| Affine::translate(-bbox.origin().to_vec2()) * p)
.collect();
let mut segments = Segments::default();
segments.add_bez_path(&bez).unwrap();
segments.check_invariants();
let eps = 16.0;
let mut range_bufs = SweepLineRangeBuffers::default();
let mut line_bufs = SweepLineBuffers::default();
// Run through once just to count the ranges.
let mut sweep_state = Sweeper::new(&segments, eps);
let mut num_ranges = 0;
while let Some(mut line) = sweep_state.next_line(&mut line_bufs) {
while line.next_range(&mut range_bufs, &segments).is_some() {
num_ranges += 1;
}
}
let pad = 32.0;
let mut sweep_state = Sweeper::new(&segments, eps);
let mut pixmap = Pixmap::new(
(bbox.width() + 2.0 * pad).ceil() as u32,
((bbox.height() + pad) * num_ranges as f64 + pad).ceil() as u32,
)
.unwrap();
let mut b = Rect::new(pad, pad, pad + bbox.width(), pad + bbox.height());
while let Some(mut line) = sweep_state.next_line(&mut line_bufs) {
while let Some(range) = line.next_range(&mut range_bufs, &segments) {
draw_orig_path(&mut pixmap, &bez, b.origin());
draw_sweep_line_range(&mut pixmap, &segments, range, b, pad);
b = b + Vec2::new(0.0, bbox.height() + pad);
}
}
let base_path = input_path_base(&path);
let out_path = output_path_for(base_path, false);
std::fs::create_dir_all(out_path.parent().unwrap()).unwrap();
pixmap.save_png(&out_path).unwrap();
let new_image = kompari::load_image(&out_path)?;
let snapshot = kompari::load_image(&saved_snapshot_path_for(base_path, false))?;
match kompari::compare_images(&snapshot, &new_image) {
kompari::ImageDifference::None => Ok(()),
_ => Err("image comparison failed".into()),
}
}
fn generate_position_snapshot(path: PathBuf, correct: bool) -> Result<(), Failed> {
let input = std::fs::read_to_string(&path).unwrap();
let tree = usvg::Tree::from_str(&input, &usvg::Options::default()).unwrap();
let bezs = linesweeper_util::svg_to_bezpaths(&tree);
let bbox = linesweeper_util::bezier_bounding_box(bezs.iter());
let bez: BezPath = bezs
.into_iter()
.flat_map(|p| Affine::translate(-bbox.origin().to_vec2()) * p)
.collect();
let eps = 16.0;
let correctness = if correct { Correctness::ReallyCorrect } else { Correctness::GoodEnough };
let top =
Topology::<i32>::from_paths_with_correctness([(&bez, ())], eps, correctness).unwrap();
let out_paths = top.compute_positions();
let pad = 2.0 * eps;
let bbox = top.bounding_box();
let mut pixmap = Pixmap::new(
(bbox.width() + 2.0 * pad).ceil() as u32,
(bbox.height() + 2.0 * pad).ceil() as u32,
)
.unwrap();
let pad_transform = tiny_skia::Transform::from_translate(
(pad - bbox.min_x()) as f32,
(pad - bbox.min_y()) as f32,
);
let stroke = tiny_skia::Stroke {
width: 1.0,
..Default::default()
};
for out_idx in top.segment_indices() {
let path = &out_paths[out_idx].path;
let far_idx = &out_paths[out_idx].copied_idx;
for (idx, seg) in path.segments().enumerate() {
let skia_seg = skia_kurbo_seg(seg);
let c = if far_idx == &Some(idx) {
path_color(0)
} else {
path_color(3)
};
pixmap.stroke_path(&skia_seg, &color(c), &stroke, pad_transform, None);
let p0 = seg.start();
let p0 = tiny_skia::PathBuilder::from_circle(p0.x as f32, p0.y as f32, 2.0).unwrap();
let p1 = seg.end();
let p1 = tiny_skia::PathBuilder::from_circle(p1.x as f32, p1.y as f32, 2.0).unwrap();
let black = color(tiny_skia::Color::BLACK);
pixmap.fill_path(
&p0,
&black,
tiny_skia::FillRule::Winding,
pad_transform,
None,
);
pixmap.fill_path(
&p1,
&black,
tiny_skia::FillRule::Winding,
pad_transform,
None,
);
}
}
let base_path = input_path_base(&path);
let out_path = output_path_for(base_path, correct);
std::fs::create_dir_all(out_path.parent().unwrap()).unwrap();
pixmap.save_png(&out_path).unwrap();
let new_image = kompari::load_image(&out_path)?;
let snapshot = kompari::load_image(&saved_snapshot_path_for(base_path, correct))?;
match kompari::compare_images(&snapshot, &new_image) {
kompari::ImageDifference::None => Ok(()),
_ => Err("image comparison failed".into()),
}
}
fn generate_output_snapshot(path: PathBuf, correct: bool) -> Result<(), Failed> {
let input = std::fs::read_to_string(&path).unwrap();
let tree = usvg::Tree::from_str(&input, &usvg::Options::default()).unwrap();
let bezs = linesweeper_util::svg_to_bezpaths(&tree);
let bbox = linesweeper_util::bezier_bounding_box(bezs.iter());
let bez: BezPath = bezs
.into_iter()
.flat_map(|p| Affine::translate(-bbox.origin().to_vec2()) * p)
.collect();
let eps = 1.0;
let correctness = if correct { Correctness::ReallyCorrect } else { Correctness::GoodEnough };
let top =
Topology::<i32>::from_paths_with_correctness([(&bez, ())], eps, correctness).unwrap();
let contours = top.contours(|w| w % 2 == 0);
let pad = 2.0 * eps;
let bbox = top.bounding_box();
let mut pixmap = Pixmap::new(
(bbox.width() + 2.0 * pad).ceil() as u32,
(bbox.height() + 2.0 * pad).ceil() as u32,
)
.unwrap();
let pad_transform = tiny_skia::Transform::from_translate(
(pad - bbox.min_x()) as f32,
(pad - bbox.min_y()) as f32,
);
let stroke = tiny_skia::Stroke {
width: 1.0,
..Default::default()
};
for contour in contours.contours() {
for seg in contour.path.segments() {
let skia_seg = skia_kurbo_seg(seg);
pixmap.stroke_path(
&skia_seg,
&color(path_color(0)),
&stroke,
pad_transform,
None,
);
let p0 = seg.start();
let p0 = tiny_skia::PathBuilder::from_circle(p0.x as f32, p0.y as f32, 2.0).unwrap();
let p1 = seg.end();
let p1 = tiny_skia::PathBuilder::from_circle(p1.x as f32, p1.y as f32, 2.0).unwrap();
let black = color(tiny_skia::Color::BLACK);
pixmap.fill_path(
&p0,
&black,
tiny_skia::FillRule::Winding,
pad_transform,
None,
);
pixmap.fill_path(
&p1,
&black,
tiny_skia::FillRule::Winding,
pad_transform,
None,
);
}
}
let base_path = input_path_base(&path);
let out_path = output_path_for(base_path, correct);
std::fs::create_dir_all(out_path.parent().unwrap()).unwrap();
pixmap.save_png(&out_path).unwrap();
let new_image = kompari::load_image(&out_path)?;
let snapshot = kompari::load_image(&saved_snapshot_path_for(base_path, correct))?;
match kompari::compare_images(&snapshot, &new_image) {
kompari::ImageDifference::None => Ok(()),
_ => Err("image comparison failed".into()),
}
}