From 9079a81b66c114009bb2b1605eb2a4e74aa0e6ab Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sat, 16 Aug 2025 16:15:57 -0700 Subject: [PATCH] Remove the Bezier-rs library from the repo --- .github/workflows/website.yml | 40 +- .gitignore | 1 + .vscode/settings.json | 2 +- Cargo.lock | 22 - Cargo.toml | 3 - libraries/bezier-rs/Cargo.toml | 29 - libraries/bezier-rs/LICENSE-APACHE | 201 - libraries/bezier-rs/LICENSE-MIT | 17 - libraries/bezier-rs/README.md | 28 - libraries/bezier-rs/src/bezier/core.rs | 278 - libraries/bezier-rs/src/bezier/lookup.rs | 362 -- .../bezier-rs/src/bezier/manipulators.rs | 79 - libraries/bezier-rs/src/bezier/mod.rs | 147 - libraries/bezier-rs/src/bezier/solvers.rs | 1175 ---- libraries/bezier-rs/src/bezier/structs.rs | 71 - libraries/bezier-rs/src/bezier/transform.rs | 1053 ---- libraries/bezier-rs/src/compare.rs | 40 - libraries/bezier-rs/src/consts.rs | 28 - libraries/bezier-rs/src/lib.rs | 15 - libraries/bezier-rs/src/poisson_disk.rs | 369 -- libraries/bezier-rs/src/polynomial.rs | 264 - libraries/bezier-rs/src/subpath/core.rs | 602 -- libraries/bezier-rs/src/subpath/lookup.rs | 519 -- .../bezier-rs/src/subpath/manipulators.rs | 231 - libraries/bezier-rs/src/subpath/mod.rs | 75 - libraries/bezier-rs/src/subpath/solvers.rs | 1067 ---- libraries/bezier-rs/src/subpath/structs.rs | 146 - libraries/bezier-rs/src/subpath/transform.rs | 1115 ---- libraries/bezier-rs/src/utils.rs | 423 -- node-graph/gcore/src/vector/vector_nodes.rs | 2 +- node-graph/gcore/src/vector/vector_types.rs | 6 +- .../graster-nodes/src/generate_curves.rs | 4 +- website/.gitignore | 1 - .../generate-editor-structure.js} | 2 +- website/{ => build-scripts}/install-fonts.js | 12 +- .../guide/student-projects/_index.md | 2 +- website/other/bezier-rs-demos/.eslintrc.cjs | 87 - website/other/bezier-rs-demos/.gitignore | 5 - website/other/bezier-rs-demos/.prettierrc | 6 - website/other/bezier-rs-demos/README.md | 18 - website/other/bezier-rs-demos/index.html | 13 - .../bezier-rs-demos/package-installer.js | 41 - .../other/bezier-rs-demos/package-lock.json | 5181 ----------------- website/other/bezier-rs-demos/package.json | 41 - .../other/bezier-rs-demos/public/style.css | 206 - .../bezier-rs-demos/src/features-bezier.ts | 589 -- .../bezier-rs-demos/src/features-subpath.ts | 252 - website/other/bezier-rs-demos/src/main.ts | 345 -- website/other/bezier-rs-demos/src/types.ts | 241 - website/other/bezier-rs-demos/tsconfig.json | 28 - website/other/bezier-rs-demos/vite.config.ts | 19 - website/other/bezier-rs-demos/wasm/Cargo.toml | 50 - .../other/bezier-rs-demos/wasm/src/bezier.rs | 723 --- website/other/bezier-rs-demos/wasm/src/lib.rs | 71 - .../other/bezier-rs-demos/wasm/src/subpath.rs | 585 -- .../bezier-rs-demos/wasm/src/svg_drawing.rs | 69 - .../other/bezier-rs-demos/wasm/src/utils.rs | 29 - website/other/build.sh | 34 - website/package.json | 3 +- website/templates/macros/replacements.html | 10 +- 60 files changed, 24 insertions(+), 17053 deletions(-) delete mode 100644 libraries/bezier-rs/Cargo.toml delete mode 100644 libraries/bezier-rs/LICENSE-APACHE delete mode 100644 libraries/bezier-rs/LICENSE-MIT delete mode 100644 libraries/bezier-rs/README.md delete mode 100644 libraries/bezier-rs/src/bezier/core.rs delete mode 100644 libraries/bezier-rs/src/bezier/lookup.rs delete mode 100644 libraries/bezier-rs/src/bezier/manipulators.rs delete mode 100644 libraries/bezier-rs/src/bezier/mod.rs delete mode 100644 libraries/bezier-rs/src/bezier/solvers.rs delete mode 100644 libraries/bezier-rs/src/bezier/structs.rs delete mode 100644 libraries/bezier-rs/src/bezier/transform.rs delete mode 100644 libraries/bezier-rs/src/compare.rs delete mode 100644 libraries/bezier-rs/src/consts.rs delete mode 100644 libraries/bezier-rs/src/lib.rs delete mode 100644 libraries/bezier-rs/src/poisson_disk.rs delete mode 100644 libraries/bezier-rs/src/polynomial.rs delete mode 100644 libraries/bezier-rs/src/subpath/core.rs delete mode 100644 libraries/bezier-rs/src/subpath/lookup.rs delete mode 100644 libraries/bezier-rs/src/subpath/manipulators.rs delete mode 100644 libraries/bezier-rs/src/subpath/mod.rs delete mode 100644 libraries/bezier-rs/src/subpath/solvers.rs delete mode 100644 libraries/bezier-rs/src/subpath/structs.rs delete mode 100644 libraries/bezier-rs/src/subpath/transform.rs delete mode 100644 libraries/bezier-rs/src/utils.rs rename website/{other/editor-structure/generate.js => build-scripts/generate-editor-structure.js} (98%) rename website/{ => build-scripts}/install-fonts.js (92%) delete mode 100644 website/other/bezier-rs-demos/.eslintrc.cjs delete mode 100644 website/other/bezier-rs-demos/.gitignore delete mode 100644 website/other/bezier-rs-demos/.prettierrc delete mode 100644 website/other/bezier-rs-demos/README.md delete mode 100644 website/other/bezier-rs-demos/index.html delete mode 100644 website/other/bezier-rs-demos/package-installer.js delete mode 100644 website/other/bezier-rs-demos/package-lock.json delete mode 100644 website/other/bezier-rs-demos/package.json delete mode 100644 website/other/bezier-rs-demos/public/style.css delete mode 100644 website/other/bezier-rs-demos/src/features-bezier.ts delete mode 100644 website/other/bezier-rs-demos/src/features-subpath.ts delete mode 100644 website/other/bezier-rs-demos/src/main.ts delete mode 100644 website/other/bezier-rs-demos/src/types.ts delete mode 100644 website/other/bezier-rs-demos/tsconfig.json delete mode 100644 website/other/bezier-rs-demos/vite.config.ts delete mode 100644 website/other/bezier-rs-demos/wasm/Cargo.toml delete mode 100644 website/other/bezier-rs-demos/wasm/src/bezier.rs delete mode 100644 website/other/bezier-rs-demos/wasm/src/lib.rs delete mode 100644 website/other/bezier-rs-demos/wasm/src/subpath.rs delete mode 100644 website/other/bezier-rs-demos/wasm/src/svg_drawing.rs delete mode 100644 website/other/bezier-rs-demos/wasm/src/utils.rs delete mode 100644 website/other/build.sh diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 5e5c27ffbc..58559c4311 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -63,14 +63,14 @@ jobs: mkdir artifacts mv hierarchical_message_system_tree.txt artifacts/hierarchical_message_system_tree.txt - - name: 🚚 Move `artifacts` contents to `website/other/editor-structure` + - name: 🚚 Move `artifacts` contents to the project root run: | - mv artifacts/* website/other/editor-structure + mv artifacts/* . - name: 🔧 Build auto-generated code docs artifacts into HTML run: | - cd website/other/editor-structure - node generate.js hierarchical_message_system_tree.txt replacement.html + cd website + npm run generate-editor-structure - name: 🌐 Build Graphite website with Zola env: @@ -80,38 +80,6 @@ jobs: npm run install-fonts zola --config config.toml build --minify - - name: 💿 Restore cache of `website/other/dist` directory, if available and `website/other` didn't change - if: steps.changes.outputs.website-other != 'true' - id: cache-website-other-dist - uses: actions/cache/restore@v3 - with: - path: website/other/dist - key: website-other-dist-${{ runner.os }} - - - name: 🟢 Set up Node only if we are going to build in the next step - if: steps.cache-website-other-dist.outputs.cache-hit != 'true' - uses: actions/setup-node@v4 - with: - node-version: "latest" - - - name: 📁 Build `website/other` directory only if changed or not cached - if: steps.cache-website-other-dist.outputs.cache-hit != 'true' - id: build-website-other - run: | - sh website/other/build.sh - - - name: 💾 Save cache of `website/other/dist` directory if it was built above - if: steps.cache-website-other-dist.outputs.cache-hit != 'true' - uses: actions/cache/save@v3 - with: - path: website/other/dist - key: ${{ steps.cache-website-other-dist.outputs.cache-primary-key }} - - - name: 🚚 Move `website/other/dist` contents to `website/public` - run: | - mkdir -p website/public - mv website/other/dist/* website/public - - name: 📤 Publish to Cloudflare Pages id: cloudflare uses: cloudflare/pages-action@1 diff --git a/.gitignore b/.gitignore index 3fa518041e..29af1eed11 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ flamegraph.svg .idea/ .direnv hierarchical_message_system_tree.txt +hierarchical_message_system_tree.html diff --git a/.vscode/settings.json b/.vscode/settings.json index 7f48a1c56a..8171f50c61 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -35,7 +35,7 @@ "rust-analyzer.cargo.allTargets": false, // ESLint config "eslint.format.enable": true, - "eslint.workingDirectories": ["./frontend", "./website/other/bezier-rs-demos", "./website"], + "eslint.workingDirectories": ["./frontend", "./website"], "eslint.validate": ["javascript", "typescript", "svelte"], // Svelte config "svelte.plugin.svelte.compilerWarnings": { diff --git a/Cargo.lock b/Cargo.lock index b8ffa99bf9..6ea1907834 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -461,28 +461,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "bezier-rs" -version = "0.4.0" -dependencies = [ - "dyn-any", - "glam", - "kurbo", - "poly-cool", - "serde", -] - -[[package]] -name = "bezier-rs-wasm" -version = "0.0.0" -dependencies = [ - "bezier-rs", - "glam", - "js-sys", - "log", - "wasm-bindgen", -] - [[package]] name = "bincode" version = "1.3.3" diff --git a/Cargo.toml b/Cargo.toml index 84bf949fca..88358d8e81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,9 +20,7 @@ members = [ "node-graph/preprocessor", "libraries/dyn-any", "libraries/path-bool", - "libraries/bezier-rs", "libraries/math-parser", - "website/other/bezier-rs-demos/wasm", ] default-members = [ "editor", @@ -44,7 +42,6 @@ resolver = "2" [workspace.dependencies] # Local dependencies -bezier-rs = { path = "libraries/bezier-rs", features = ["dyn-any", "serde"] } dyn-any = { path = "libraries/dyn-any", features = [ "derive", "glam", diff --git a/libraries/bezier-rs/Cargo.toml b/libraries/bezier-rs/Cargo.toml deleted file mode 100644 index e030c5d8a7..0000000000 --- a/libraries/bezier-rs/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "bezier-rs" -version = "0.4.0" -rust-version = "1.85" -edition = "2024" -authors = ["Graphite Authors "] -description = "Computational geometry algorithms for Bézier segments and shapes useful in the context of 2D graphics" -license = "MIT OR Apache-2.0" -readme = "README.md" -keywords = ["bezier", "curve", "geometry", "2d", "graphics"] -categories = ["graphics", "mathematics"] -homepage = "https://github.com/GraphiteEditor/Graphite/tree/master/libraries/bezier-rs" -repository = "https://github.com/GraphiteEditor/Graphite/tree/master/libraries/bezier-rs" -documentation = "https://graphite.rs/libraries/bezier-rs/" - -[features] -std = ["glam/std"] - -[dependencies] -# Required dependencies -glam = { workspace = true } -poly-cool = { workspace = true } - -# Optional local dependencies -dyn-any = { version = "0.3.0", path = "../dyn-any", optional = true } - -# Optional workspace dependencies -kurbo = { workspace = true, optional = true } -serde = { workspace = true, optional = true } diff --git a/libraries/bezier-rs/LICENSE-APACHE b/libraries/bezier-rs/LICENSE-APACHE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/libraries/bezier-rs/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - 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. diff --git a/libraries/bezier-rs/LICENSE-MIT b/libraries/bezier-rs/LICENSE-MIT deleted file mode 100644 index 969d061e8b..0000000000 --- a/libraries/bezier-rs/LICENSE-MIT +++ /dev/null @@ -1,17 +0,0 @@ -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. diff --git a/libraries/bezier-rs/README.md b/libraries/bezier-rs/README.md deleted file mode 100644 index e7856741e5..0000000000 --- a/libraries/bezier-rs/README.md +++ /dev/null @@ -1,28 +0,0 @@ -[crates.io](https://crates.io/crates/bezier-rs) • [docs.rs](https://docs.rs/bezier-rs/latest/bezier_rs/) • [repo](https://github.com/GraphiteEditor/Graphite/tree/master/libraries/bezier-rs) - -# Bezier-rs - -Computational geometry algorithms for Bézier segments and shapes useful in the context of 2D graphics. - -Play with the interactive documentation which visualizes each API function in a fun manner: - -### [**View the interactive API**](https://graphite.rs/libraries/bezier-rs/) - ---- - -Bezier-rs is built for the needs of [Graphite](https://graphite.rs), an open source 2D vector graphics editor. We hope it may be useful to others, but presently Graphite is its primary user. Pull requests are welcomed for new features, code cleanup, ergonomic enhancements, performance improvements, and documentation clarifications. - -The library currently provides functions dealing with single Bézier curve segments and open-or-closed multi-segment paths (which we call _subpaths_). - -In the future, the library will be expanded to include compound paths (multiple subpaths forming a single shape, where the winding order determines inside-or-outside-ness) and operations between paths (e.g. boolean operations, convex hull). Pull requests for these additional features would be highly desirable. - -Bezier-rs is inspired by [Bezier.js](https://pomax.github.io/bezierjs/) and [_A Primer on Bézier Curves_](https://pomax.github.io/bezierinfo/) by Pomax. Bezier-rs is not a port of Bezier.js so the API for single-segment Bézier curves has some differences, and the intention is to offer a broader scope that provides algorithms beyond single curve segments (as noted above) to eventually service full vector shapes. - -## Terminology - -Graphite and Bezier-rs use the following terminology for vector data. These depictions are given for cubic Bézier curves. - -![Manipulators](https://static.graphite.rs/libraries/bezier-rs/manipulator-groups.png) -![Curve/Bezier Segment](https://static.graphite.rs/libraries/bezier-rs/curve-bezier-segment.png) -![Subpath/Path](https://static.graphite.rs/libraries/bezier-rs/subpath-path.png) -![Open/Closed](https://static.graphite.rs/libraries/bezier-rs/closed-open-subpath.png) diff --git a/libraries/bezier-rs/src/bezier/core.rs b/libraries/bezier-rs/src/bezier/core.rs deleted file mode 100644 index 123cfe458f..0000000000 --- a/libraries/bezier-rs/src/bezier/core.rs +++ /dev/null @@ -1,278 +0,0 @@ -use super::*; -use std::fmt::Write; -use utils::format_point; - -/// Functionality relating to core `Bezier` operations, such as constructors and `abs_diff_eq`. -impl Bezier { - // TODO: Consider removing this function - /// Create a linear bezier using the provided coordinates as the start and end points. - pub fn from_linear_coordinates(x1: f64, y1: f64, x2: f64, y2: f64) -> Self { - Bezier { - start: DVec2::new(x1, y1), - handles: BezierHandles::Linear, - end: DVec2::new(x2, y2), - } - } - - /// Create a linear bezier using the provided DVec2s as the start and end points. - /// - pub fn from_linear_dvec2(p1: DVec2, p2: DVec2) -> Self { - Bezier { - start: p1, - handles: BezierHandles::Linear, - end: p2, - } - } - - // TODO: Consider removing this function - /// Create a quadratic bezier using the provided coordinates as the start, handle, and end points. - pub fn from_quadratic_coordinates(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> Self { - Bezier { - start: DVec2::new(x1, y1), - handles: BezierHandles::Quadratic { handle: DVec2::new(x2, y2) }, - end: DVec2::new(x3, y3), - } - } - - /// Create a quadratic bezier using the provided DVec2s as the start, handle, and end points. - pub fn from_quadratic_dvec2(p1: DVec2, p2: DVec2, p3: DVec2) -> Self { - Bezier { - start: p1, - handles: BezierHandles::Quadratic { handle: p2 }, - end: p3, - } - } - - // TODO: Consider removing this function - /// Create a cubic bezier using the provided coordinates as the start, handles, and end points. - #[allow(clippy::too_many_arguments)] - pub fn from_cubic_coordinates(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, x4: f64, y4: f64) -> Self { - Bezier { - start: DVec2::new(x1, y1), - handles: BezierHandles::Cubic { - handle_start: DVec2::new(x2, y2), - handle_end: DVec2::new(x3, y3), - }, - end: DVec2::new(x4, y4), - } - } - - /// Create a cubic bezier using the provided DVec2s as the start, handles, and end points. - pub fn from_cubic_dvec2(p1: DVec2, p2: DVec2, p3: DVec2, p4: DVec2) -> Self { - Bezier { - start: p1, - handles: BezierHandles::Cubic { handle_start: p2, handle_end: p3 }, - end: p4, - } - } - - /// Create a quadratic bezier curve that goes through 3 points, where the middle point will be at the corresponding position `t` on the curve. - /// - `t` - A representation of how far along the curve the provided point should occur at. The default value is 0.5. - /// - /// Note that when `t = 0` or `t = 1`, the expectation is that the `point_on_curve` should be equal to `start` and `end` respectively. - /// In these cases, if the provided values are not equal, this function will use the `point_on_curve` as the `start`/`end` instead. - /// - pub fn quadratic_through_points(start: DVec2, point_on_curve: DVec2, end: DVec2, t: Option) -> Self { - let t = t.unwrap_or(DEFAULT_T_VALUE); - if t == 0. { - return Bezier::from_quadratic_dvec2(point_on_curve, point_on_curve, end); - } - if t == 1. { - return Bezier::from_quadratic_dvec2(start, point_on_curve, point_on_curve); - } - let [a, _, _] = utils::compute_abc_for_quadratic_through_points(start, point_on_curve, end, t); - Bezier::from_quadratic_dvec2(start, a, end) - } - - /// Create a cubic bezier curve that goes through 3 points, where the middle point will be at the corresponding position `t` on the curve. - /// - `t` - A representation of how far along the curve the provided point should occur at. The default value is 0.5. - /// - /// Note that when `t = 0` or `t = 1`, the expectation is that the `point_on_curve` should be equal to `start` and `end` respectively. - /// In these cases, if the provided values are not equal, this function will use the `point_on_curve` as the `start`/`end` instead. - /// - `midpoint_separation` - A representation of how wide the resulting curve will be around `t` on the curve. This parameter designates the distance between the `e1` and `e2` defined in [the projection identity section](https://pomax.github.io/bezierinfo/#abc) of Pomax's bezier curve primer. It is an optional parameter and the default value is the distance between the points `B` and `C` defined in the primer. - pub fn cubic_through_points(start: DVec2, point_on_curve: DVec2, end: DVec2, t: Option, midpoint_separation: Option) -> Self { - let t = t.unwrap_or(DEFAULT_T_VALUE); - if t == 0. { - return Bezier::from_cubic_dvec2(point_on_curve, point_on_curve, end, end); - } - if t == 1. { - return Bezier::from_cubic_dvec2(start, start, point_on_curve, point_on_curve); - } - let [a, b, c] = utils::compute_abc_for_cubic_through_points(start, point_on_curve, end, t); - let midpoint_separation = midpoint_separation.unwrap_or_else(|| b.distance(c)); - let distance_between_start_and_end = (end - start) / (start.distance(end)); - let e1 = b - (distance_between_start_and_end * midpoint_separation); - let e2 = b + (distance_between_start_and_end * midpoint_separation * (1. - t) / t); - - // TODO: these functions can be changed to helpers, but need to come up with an appropriate name first - let v1 = (e1 - t * a) / (1. - t); - let v2 = (e2 - (1. - t) * a) / t; - let handle_start = (v1 - (1. - t) * start) / t; - let handle_end = (v2 - t * end) / (1. - t); - Bezier::from_cubic_dvec2(start, handle_start, handle_end, end) - } - - /// Return the string argument used to create a curve in an SVG `path`, excluding the start point. - pub fn svg_curve_argument(&self) -> String { - let mut out = String::new(); - self.write_curve_argument(&mut out).unwrap(); - out - } - - /// Write the curve argument to the string - pub fn write_curve_argument(&self, svg: &mut String) -> std::fmt::Result { - match self.handles { - BezierHandles::Linear => svg.push_str(SVG_ARG_LINEAR), - BezierHandles::Quadratic { handle } => { - format_point(svg, SVG_ARG_QUADRATIC, handle.x, handle.y)?; - } - BezierHandles::Cubic { handle_start, handle_end } => { - format_point(svg, SVG_ARG_CUBIC, handle_start.x, handle_start.y)?; - format_point(svg, " ", handle_end.x, handle_end.y)?; - } - } - format_point(svg, " ", self.end.x, self.end.y) - } - - /// Return the string argument used to create the lines connecting handles to endpoints in an SVG `path` - pub(crate) fn svg_handle_line_argument(&self) -> Option { - let mut result = String::new(); - - match self.handles { - BezierHandles::Linear => {} - BezierHandles::Quadratic { handle } => { - let _ = format_point(&mut result, SVG_ARG_MOVE, self.start.x, self.start.y); - let _ = format_point(&mut result, SVG_ARG_LINEAR, handle.x, handle.y); - let _ = format_point(&mut result, SVG_ARG_MOVE, self.end.x, self.end.y); - let _ = format_point(&mut result, SVG_ARG_LINEAR, handle.x, handle.y); - } - BezierHandles::Cubic { handle_start, handle_end } => { - let _ = format_point(&mut result, SVG_ARG_MOVE, self.start.x, self.start.y); - let _ = format_point(&mut result, SVG_ARG_LINEAR, handle_start.x, handle_start.y); - let _ = format_point(&mut result, SVG_ARG_MOVE, self.end.x, self.end.y); - let _ = format_point(&mut result, SVG_ARG_LINEAR, handle_end.x, handle_end.y); - } - } - - (!result.is_empty()).then_some(result) - } - - /// Appends to the `svg` mutable string with an SVG shape representation of the curve. - pub fn curve_to_svg(&self, svg: &mut String, attributes: String) { - let _ = write!(svg, r#""#, self.start.x, self.start.y, self.svg_curve_argument(), attributes); - } - - /// Appends to the `svg` mutable string with an SVG shape representation of the handle lines. - pub fn handle_lines_to_svg(&self, svg: &mut String, attributes: String) { - let _ = write!(svg, r#""#, self.svg_handle_line_argument().unwrap_or_default(), attributes); - } - - /// Appends to the `svg` mutable string with an SVG shape representation of the anchors. - pub fn anchors_to_svg(&self, svg: &mut String, attributes: String) { - let _ = write!( - svg, - r#""#, - self.start.x, self.start.y, self.end.x, self.end.y - ); - } - - /// Appends to the `svg` mutable string with an SVG shape representation of the handles. - pub fn handles_to_svg(&self, svg: &mut String, attributes: String) { - if let BezierHandles::Quadratic { handle } = self.handles { - let _ = write!(svg, r#""#, handle.x, handle.y); - } else if let BezierHandles::Cubic { handle_start, handle_end } = self.handles { - let _ = write!( - svg, - r#""#, - handle_start.x, handle_start.y, handle_end.x, handle_end.y - ); - }; - } - - /// Appends to the `svg` mutable string with an SVG shape representation that includes the curve, the handle lines, the anchors, and the handles. - pub fn to_svg(&self, svg: &mut String, curve_attributes: String, anchor_attributes: String, handle_attributes: String, handle_line_attributes: String) { - if !curve_attributes.is_empty() { - self.curve_to_svg(svg, curve_attributes); - } - if !handle_line_attributes.is_empty() { - self.handle_lines_to_svg(svg, handle_line_attributes); - } - if !anchor_attributes.is_empty() { - self.anchors_to_svg(svg, anchor_attributes); - } - if !handle_attributes.is_empty() { - self.handles_to_svg(svg, handle_attributes); - } - } - - /// Returns true if the corresponding points of the two `Bezier`s are within the provided absolute value difference from each other. - /// The points considered includes the start, end, and any relevant handles. - pub fn abs_diff_eq(&self, other: &Bezier, max_abs_diff: f64) -> bool { - let a = if self.is_linear() { Self::from_linear_dvec2(self.start, self.end) } else { *self }; - let b = if other.is_linear() { Self::from_linear_dvec2(other.start, other.end) } else { *other }; - - let self_points = a.get_points().collect::>(); - let other_points = b.get_points().collect::>(); - - self_points.len() == other_points.len() && self_points.into_iter().zip(other_points).all(|(a, b)| a.abs_diff_eq(b, max_abs_diff)) - } - - /// Returns true if the start, end and handles of the Bezier are all at the same location - pub fn is_point(&self) -> bool { - let start = self.start(); - - self.get_points().all(|point| point.abs_diff_eq(start, MAX_ABSOLUTE_DIFFERENCE)) - } - - /// Returns true if the Bezier curve is equivalent to a line. - /// - /// **NOTE**: This is different from simply checking if the handle is [`BezierHandles::Linear`]. A [`Quadratic`](BezierHandles::Quadratic) or [`Cubic`](BezierHandles::Cubic) Bezier curve can also be a line if the handles are colinear to the start and end points. Therefore if the handles exceed the start and end point, it will still be considered as a line. - pub fn is_linear(&self) -> bool { - let is_colinear = |a: DVec2, b: DVec2, c: DVec2| -> bool { ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)).abs() < MAX_ABSOLUTE_DIFFERENCE }; - - match self.handles { - BezierHandles::Linear => true, - BezierHandles::Quadratic { handle } => is_colinear(self.start, handle, self.end), - BezierHandles::Cubic { handle_start, handle_end } => is_colinear(self.start, handle_start, self.end) && is_colinear(self.start, handle_end, self.end), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::compare::compare_points; - use crate::utils::TValue; - - #[test] - fn test_quadratic_from_points() { - let p1 = DVec2::new(30., 50.); - let p2 = DVec2::new(140., 30.); - let p3 = DVec2::new(160., 170.); - - let bezier1 = Bezier::quadratic_through_points(p1, p2, p3, None); - assert!(compare_points(bezier1.evaluate(TValue::Parametric(0.5)), p2)); - - let bezier2 = Bezier::quadratic_through_points(p1, p2, p3, Some(0.8)); - assert!(compare_points(bezier2.evaluate(TValue::Parametric(0.8)), p2)); - - let bezier3 = Bezier::quadratic_through_points(p1, p2, p3, Some(0.)); - assert!(compare_points(bezier3.evaluate(TValue::Parametric(0.)), p2)); - } - - #[test] - fn test_cubic_through_points() { - let p1 = DVec2::new(30., 30.); - let p2 = DVec2::new(60., 140.); - let p3 = DVec2::new(160., 160.); - - let bezier1 = Bezier::cubic_through_points(p1, p2, p3, Some(0.3), Some(10.)); - assert!(compare_points(bezier1.evaluate(TValue::Parametric(0.3)), p2)); - - let bezier2 = Bezier::cubic_through_points(p1, p2, p3, Some(0.8), Some(91.7)); - assert!(compare_points(bezier2.evaluate(TValue::Parametric(0.8)), p2)); - - let bezier3 = Bezier::cubic_through_points(p1, p2, p3, Some(0.), Some(91.7)); - assert!(compare_points(bezier3.evaluate(TValue::Parametric(0.)), p2)); - } -} diff --git a/libraries/bezier-rs/src/bezier/lookup.rs b/libraries/bezier-rs/src/bezier/lookup.rs deleted file mode 100644 index 06bcf1d0cd..0000000000 --- a/libraries/bezier-rs/src/bezier/lookup.rs +++ /dev/null @@ -1,362 +0,0 @@ -use super::*; -use crate::utils::{TValue, TValueType}; - -/// Functionality relating to looking up properties of the `Bezier` or points along the `Bezier`. -impl Bezier { - /// Convert a euclidean distance ratio along the `Bezier` curve to a parametric `t`-value. - pub fn euclidean_to_parametric(&self, ratio: f64, error: f64) -> f64 { - let total_length = self.length(None); - self.euclidean_to_parametric_with_total_length(ratio, error, total_length) - } - - /// Convert a euclidean distance ratio along the `Bezier` curve to a parametric `t`-value. - /// For performance reasons, this version of the [`euclidean_to_parametric`] function allows the caller to - /// provide the total length of the curve so it doesn't have to be calculated every time the function is called. - pub fn euclidean_to_parametric_with_total_length(&self, euclidean_t: f64, error: f64, total_length: f64) -> f64 { - if euclidean_t < error { - return 0.; - } - if 1. - euclidean_t < error { - return 1.; - } - - match self.handles { - BezierHandles::Linear => euclidean_t, - BezierHandles::Quadratic { handle } => { - // Use Casteljau subdivision, noting that the length is more than the straight line distance from start to end but less than the straight line distance through the handles - fn recurse(a0: DVec2, a1: DVec2, a2: DVec2, level: u8, desired_len: f64) -> (f64, f64) { - let lower = a0.distance(a2); - let upper = a0.distance(a1) + a1.distance(a2); - if level >= 8 { - let approx_len = (lower + upper) / 2.; - return (approx_len, desired_len / approx_len); - } - - let b1 = 0.5 * (a0 + a1); - let c1 = 0.5 * (a1 + a2); - let b2 = 0.5 * (b1 + c1); - let (first_len, t) = recurse(a0, b1, b2, level + 1, desired_len); - if first_len > desired_len { - return (first_len, t * 0.5); - } - let (second_len, t) = recurse(b2, c1, a2, level + 1, desired_len - first_len); - (first_len + second_len, t * 0.5 + 0.5) - } - recurse(self.start, handle, self.end, 0, total_length * euclidean_t).1 - } - BezierHandles::Cubic { handle_start, handle_end } => { - // Use Casteljau subdivision, noting that the length is more than the straight line distance from start to end but less than the straight line distance through the handles - fn recurse(a0: DVec2, a1: DVec2, a2: DVec2, a3: DVec2, level: u8, desired_len: f64) -> (f64, f64) { - let lower = a0.distance(a3); - let upper = a0.distance(a1) + a1.distance(a2) + a2.distance(a3); - if level >= 8 { - let approx_len = (lower + upper) / 2.; - return (approx_len, desired_len / approx_len); - } - - let b1 = 0.5 * (a0 + a1); - let t0 = 0.5 * (a1 + a2); - let c1 = 0.5 * (a2 + a3); - let b2 = 0.5 * (b1 + t0); - let c2 = 0.5 * (t0 + c1); - let b3 = 0.5 * (b2 + c2); - let (first_len, t) = recurse(a0, b1, b2, b3, level + 1, desired_len); - if first_len > desired_len { - return (first_len, t * 0.5); - } - let (second_len, t) = recurse(b3, c2, c1, a3, level + 1, desired_len - first_len); - (first_len + second_len, t * 0.5 + 0.5) - } - recurse(self.start, handle_start, handle_end, self.end, 0, total_length * euclidean_t).1 - } - } - .clamp(0., 1.) - } - - /// Convert a [TValue] to a parametric `t`-value. - pub(crate) fn t_value_to_parametric(&self, t: TValue) -> f64 { - match t { - TValue::Parametric(t) => { - assert!((0.0..=1.).contains(&t)); - t - } - TValue::Euclidean(t) => { - assert!((0.0..=1.).contains(&t)); - self.euclidean_to_parametric(t, DEFAULT_EUCLIDEAN_ERROR_BOUND) - } - TValue::EuclideanWithinError { t, error } => { - assert!((0.0..=1.).contains(&t)); - self.euclidean_to_parametric(t, error) - } - } - } - - /// Calculate the point on the curve based on the `t`-value provided. - pub(crate) fn unrestricted_parametric_evaluate(&self, t: f64) -> DVec2 { - // Basis code based off of pseudocode found here: . - - let t_squared = t * t; - let one_minus_t = 1. - t; - let squared_one_minus_t = one_minus_t * one_minus_t; - - match self.handles { - BezierHandles::Linear => self.start.lerp(self.end, t), - BezierHandles::Quadratic { handle } => squared_one_minus_t * self.start + 2. * one_minus_t * t * handle + t_squared * self.end, - BezierHandles::Cubic { handle_start, handle_end } => { - let t_cubed = t_squared * t; - let cubed_one_minus_t = squared_one_minus_t * one_minus_t; - cubed_one_minus_t * self.start + 3. * squared_one_minus_t * t * handle_start + 3. * one_minus_t * t_squared * handle_end + t_cubed * self.end - } - } - } - - /// Calculate the coordinates of the point `t` along the curve. - /// Expects `t` to be within the inclusive range `[0, 1]`. - /// - pub fn evaluate(&self, t: TValue) -> DVec2 { - let t = self.t_value_to_parametric(t); - self.unrestricted_parametric_evaluate(t) - } - - /// Return a selection of equidistant points on the bezier curve. - /// If no value is provided for `steps`, then the function will default `steps` to be 10. - /// - pub fn compute_lookup_table(&self, steps: Option, tvalue_type: Option) -> impl Iterator + '_ { - let steps = steps.unwrap_or(DEFAULT_LUT_STEP_SIZE); - let tvalue_type = tvalue_type.unwrap_or(TValueType::Parametric); - - (0..=steps).map(move |t| { - let tvalue = match tvalue_type { - TValueType::Parametric => TValue::Parametric(t as f64 / steps as f64), - TValueType::Euclidean => TValue::Euclidean(t as f64 / steps as f64), - }; - self.evaluate(tvalue) - }) - } - - /// Return an approximation of the length of the bezier curve. - /// - `tolerance` - Tolerance used to approximate the curve. - /// - pub fn length(&self, tolerance: Option) -> f64 { - match self.handles { - BezierHandles::Linear => (self.start - self.end).length(), - BezierHandles::Quadratic { handle } => { - // Use Casteljau subdivision, noting that the length is more than the straight line distance from start to end but less than the straight line distance through the handles - fn recurse(a0: DVec2, a1: DVec2, a2: DVec2, tolerance: f64, level: u8) -> f64 { - let lower = a0.distance(a2); - let upper = a0.distance(a1) + a1.distance(a2); - if upper - lower <= 2. * tolerance || level >= 8 { - return (lower + upper) / 2.; - } - - let b1 = 0.5 * (a0 + a1); - let c1 = 0.5 * (a1 + a2); - let b2 = 0.5 * (b1 + c1); - recurse(a0, b1, b2, 0.5 * tolerance, level + 1) + recurse(b2, c1, a2, 0.5 * tolerance, level + 1) - } - recurse(self.start, handle, self.end, tolerance.unwrap_or_default(), 0) - } - BezierHandles::Cubic { handle_start, handle_end } => { - // Use Casteljau subdivision, noting that the length is more than the straight line distance from start to end but less than the straight line distance through the handles - fn recurse(a0: DVec2, a1: DVec2, a2: DVec2, a3: DVec2, tolerance: f64, level: u8) -> f64 { - let lower = a0.distance(a3); - let upper = a0.distance(a1) + a1.distance(a2) + a2.distance(a3); - if upper - lower <= 2. * tolerance || level >= 8 { - return (lower + upper) / 2.; - } - - let b1 = 0.5 * (a0 + a1); - let t0 = 0.5 * (a1 + a2); - let c1 = 0.5 * (a2 + a3); - let b2 = 0.5 * (b1 + t0); - let c2 = 0.5 * (t0 + c1); - let b3 = 0.5 * (b2 + c2); - recurse(a0, b1, b2, b3, 0.5 * tolerance, level + 1) + recurse(b3, c2, c1, a3, 0.5 * tolerance, level + 1) - } - recurse(self.start, handle_start, handle_end, self.end, tolerance.unwrap_or_default(), 0) - } - } - } - - /// Return an approximation of the length centroid, together with the length, of the bezier curve. - /// - /// The length centroid is the center of mass for the arc length of the Bezier segment. - /// An infinitely thin wire forming the Bezier segment's shape would balance at this point. - /// - /// - `tolerance` - Tolerance used to approximate the curve. - pub fn length_centroid_and_length(&self, tolerance: Option) -> (DVec2, f64) { - match self.handles { - BezierHandles::Linear => ((self.start + self.end()) / 2., (self.start - self.end).length()), - BezierHandles::Quadratic { handle } => { - // Use Casteljau subdivision, noting that the length is more than the straight line distance from start to end but less than the straight line distance through the handles - fn recurse(a0: DVec2, a1: DVec2, a2: DVec2, tolerance: f64, level: u8) -> (f64, DVec2) { - let lower = a0.distance(a2); - let upper = a0.distance(a1) + a1.distance(a2); - if upper - lower <= 2. * tolerance || level >= 8 { - let length = (lower + upper) / 2.; - return (length, length * (a0 + a1 + a2) / 3.); - } - - let b1 = 0.5 * (a0 + a1); - let c1 = 0.5 * (a1 + a2); - let b2 = 0.5 * (b1 + c1); - - let (length1, centroid_part1) = recurse(a0, b1, b2, 0.5 * tolerance, level + 1); - let (length2, centroid_part2) = recurse(b2, c1, a2, 0.5 * tolerance, level + 1); - (length1 + length2, centroid_part1 + centroid_part2) - } - - let (length, centroid_parts) = recurse(self.start, handle, self.end, tolerance.unwrap_or_default(), 0); - (centroid_parts / length, length) - } - BezierHandles::Cubic { handle_start, handle_end } => { - // Use Casteljau subdivision, noting that the length is more than the straight line distance from start to end but less than the straight line distance through the handles - fn recurse(a0: DVec2, a1: DVec2, a2: DVec2, a3: DVec2, tolerance: f64, level: u8) -> (f64, DVec2) { - let lower = a0.distance(a3); - let upper = a0.distance(a1) + a1.distance(a2) + a2.distance(a3); - if upper - lower <= 2. * tolerance || level >= 8 { - let length = (lower + upper) / 2.; - return (length, length * (a0 + a1 + a2 + a3) / 4.); - } - - let b1 = 0.5 * (a0 + a1); - let t0 = 0.5 * (a1 + a2); - let c1 = 0.5 * (a2 + a3); - let b2 = 0.5 * (b1 + t0); - let c2 = 0.5 * (t0 + c1); - let b3 = 0.5 * (b2 + c2); - - let (length1, centroid_part1) = recurse(a0, b1, b2, b3, 0.5 * tolerance, level + 1); - let (length2, centroid_part2) = recurse(b3, c2, c1, a3, 0.5 * tolerance, level + 1); - (length1 + length2, centroid_part1 + centroid_part2) - } - let (length, centroid_parts) = recurse(self.start, handle_start, handle_end, self.end, tolerance.unwrap_or_default(), 0); - (centroid_parts / length, length) - } - } - } - - /// Return an approximation of the length centroid of the Bezier curve. - /// - /// The length centroid is the center of mass for the arc length of the Bezier segment. - /// An infinitely thin wire with the Bezier segment's shape would balance at this point. - /// - /// - `tolerance` - Tolerance used to approximate the curve. - /// - pub fn length_centroid(&self, tolerance: Option) -> DVec2 { - self.length_centroid_and_length(tolerance).0 - } - - /// Returns the parametric `t`-value that corresponds to the closest point on the curve to the provided point. - /// - pub fn project(&self, point: DVec2) -> f64 { - // The points at which the line from us to `point` is perpendicular - // to our curve are the critical points of the distance function. - let critical = self.normals_to_point(point); - - let mut closest = 0.; - let mut min_dist_squared = self.evaluate(TValue::Parametric(0.)).distance_squared(point); - - for time in critical { - let distance = self.evaluate(TValue::Parametric(time)).distance_squared(point); - if distance < min_dist_squared { - closest = time; - min_dist_squared = distance; - } - } - - if self.evaluate(TValue::Parametric(1.)).distance_squared(point) < min_dist_squared { - closest = 1.; - } - closest - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_evaluate() { - let p1 = DVec2::new(3., 5.); - let p2 = DVec2::new(14., 3.); - let p3 = DVec2::new(19., 14.); - let p4 = DVec2::new(30., 21.); - - let bezier1 = Bezier::from_quadratic_dvec2(p1, p2, p3); - assert_eq!(bezier1.evaluate(TValue::Parametric(0.5)), DVec2::new(12.5, 6.25)); - - let bezier2 = Bezier::from_cubic_dvec2(p1, p2, p3, p4); - assert_eq!(bezier2.evaluate(TValue::Parametric(0.5)), DVec2::new(16.5, 9.625)); - } - - #[test] - fn test_compute_lookup_table() { - let bezier1 = Bezier::from_quadratic_coordinates(10., 10., 30., 30., 50., 10.); - let lookup_table1 = bezier1.compute_lookup_table(Some(2), Some(TValueType::Parametric)).collect::>(); - assert_eq!(lookup_table1, vec![bezier1.start(), bezier1.evaluate(TValue::Parametric(0.5)), bezier1.end()]); - - let bezier2 = Bezier::from_cubic_coordinates(10., 10., 30., 30., 70., 70., 90., 10.); - let lookup_table2 = bezier2.compute_lookup_table(Some(4), Some(TValueType::Parametric)).collect::>(); - assert_eq!( - lookup_table2, - vec![ - bezier2.start(), - bezier2.evaluate(TValue::Parametric(0.25)), - bezier2.evaluate(TValue::Parametric(0.50)), - bezier2.evaluate(TValue::Parametric(0.75)), - bezier2.end() - ] - ); - } - - #[test] - fn test_length() { - let p1 = DVec2::new(30., 50.); - let p2 = DVec2::new(140., 30.); - let p3 = DVec2::new(160., 170.); - let p4 = DVec2::new(77., 129.); - - let bezier_linear = Bezier::from_linear_dvec2(p1, p2); - assert!(utils::f64_compare(bezier_linear.length(None), p1.distance(p2), MAX_ABSOLUTE_DIFFERENCE)); - - let bezier_quadratic = Bezier::from_quadratic_dvec2(p1, p2, p3); - assert!(utils::f64_compare(bezier_quadratic.length(None), 204., 1e-2)); - - let bezier_cubic = Bezier::from_cubic_dvec2(p1, p2, p3, p4); - assert!(utils::f64_compare(bezier_cubic.length(None), 199., 1e-2)); - } - - #[test] - fn test_length_centroid() { - let p1 = DVec2::new(30., 50.); - let p2 = DVec2::new(140., 30.); - let p3 = DVec2::new(160., 170.); - let p4 = DVec2::new(77., 129.); - - let bezier_linear = Bezier::from_linear_dvec2(p1, p2); - assert!(bezier_linear.length_centroid_and_length(None).0.abs_diff_eq((p1 + p2) / 2., MAX_ABSOLUTE_DIFFERENCE)); - - let bezier_quadratic = Bezier::from_quadratic_dvec2(p1, p2, p3); - let expected = DVec2::new(112.81017736920136, 87.98713052477228); - assert!(bezier_quadratic.length_centroid_and_length(None).0.abs_diff_eq(expected, MAX_ABSOLUTE_DIFFERENCE)); - - let bezier_cubic = Bezier::from_cubic_dvec2(p1, p2, p3, p4); - let expected = DVec2::new(95.23597072432115, 88.0645175770206); - assert!(bezier_cubic.length_centroid_and_length(None).0.abs_diff_eq(expected, MAX_ABSOLUTE_DIFFERENCE)); - } - - #[test] - fn test_project() { - let bezier1 = Bezier::from_cubic_coordinates(4., 4., 23., 45., 10., 30., 56., 90.); - assert_eq!(bezier1.project(DVec2::ZERO), 0.); - assert_eq!(bezier1.project(DVec2::new(100., 100.)), 1.); - - let bezier2 = Bezier::from_quadratic_coordinates(0., 0., 0., 100., 100., 100.); - assert_eq!(bezier2.project(DVec2::new(99.99, 0.)), 0.); - assert!((bezier2.project(DVec2::new(-50., 150.)) - 0.5).abs() <= 1e-8); - - let bezier3 = Bezier::from_cubic_coordinates(-50., -50., -50., -50., 50., -50., 50., -50.); - assert_eq!(DVec2::new(0., -50.), bezier3.evaluate(TValue::Parametric(bezier3.project(DVec2::new(0., -50.))))); - } -} diff --git a/libraries/bezier-rs/src/bezier/manipulators.rs b/libraries/bezier-rs/src/bezier/manipulators.rs deleted file mode 100644 index 1b70f3586b..0000000000 --- a/libraries/bezier-rs/src/bezier/manipulators.rs +++ /dev/null @@ -1,79 +0,0 @@ -use super::*; - -/// Functionality for the getters and setters of the various points in a Bezier -impl Bezier { - /// Set the coordinates of the start point. - pub fn set_start(&mut self, s: DVec2) { - self.start = s; - } - - /// Set the coordinates of the end point. - pub fn set_end(&mut self, e: DVec2) { - self.end = e; - } - - /// Set the coordinates of the first handle point. This represents the only handle in a quadratic segment. If used on a linear segment, it will be changed to a quadratic. - pub fn set_handle_start(&mut self, h1: DVec2) { - match self.handles { - BezierHandles::Linear => { - self.handles = BezierHandles::Quadratic { handle: h1 }; - } - BezierHandles::Quadratic { ref mut handle } => { - *handle = h1; - } - BezierHandles::Cubic { ref mut handle_start, .. } => { - *handle_start = h1; - } - }; - } - - /// Set the coordinates of the second handle point. This will convert both linear and quadratic segments into cubic ones. For a linear segment, the first handle will be set to the start point. - pub fn set_handle_end(&mut self, h2: DVec2) { - match self.handles { - BezierHandles::Linear => { - self.handles = BezierHandles::Cubic { - handle_start: self.start, - handle_end: h2, - }; - } - BezierHandles::Quadratic { handle } => { - self.handles = BezierHandles::Cubic { handle_start: handle, handle_end: h2 }; - } - BezierHandles::Cubic { ref mut handle_end, .. } => { - *handle_end = h2; - } - }; - } - - /// Get the coordinates of the bezier segment's start point. - pub fn start(&self) -> DVec2 { - self.start - } - - /// Get the coordinates of the bezier segment's end point. - pub fn end(&self) -> DVec2 { - self.end - } - - /// Get the coordinates of the bezier segment's first handle point. This represents the only handle in a quadratic segment. - pub fn handle_start(&self) -> Option { - self.handles.start() - } - - /// Get the coordinates of the second handle point. This will return `None` for a quadratic segment. - pub fn handle_end(&self) -> Option { - self.handles.end() - } - - /// Get an iterator over the coordinates of all points in a vector. - /// - For a linear segment, the order of the points will be: `start`, `end`. - /// - For a quadratic segment, the order of the points will be: `start`, `handle`, `end`. - /// - For a cubic segment, the order of the points will be: `start`, `handle_start`, `handle_end`, `end`. - pub fn get_points(&self) -> impl Iterator + use<> { - match self.handles { - BezierHandles::Linear => [self.start, self.end, DVec2::ZERO, DVec2::ZERO].into_iter().take(2), - BezierHandles::Quadratic { handle } => [self.start, handle, self.end, DVec2::ZERO].into_iter().take(3), - BezierHandles::Cubic { handle_start, handle_end } => [self.start, handle_start, handle_end, self.end].into_iter().take(4), - } - } -} diff --git a/libraries/bezier-rs/src/bezier/mod.rs b/libraries/bezier-rs/src/bezier/mod.rs deleted file mode 100644 index 57c320fd20..0000000000 --- a/libraries/bezier-rs/src/bezier/mod.rs +++ /dev/null @@ -1,147 +0,0 @@ -mod core; -mod lookup; -mod manipulators; -mod solvers; -mod structs; -mod transform; - -use crate::consts::*; -use crate::utils; -use glam::DVec2; -use std::fmt::{Debug, Formatter, Result}; -pub use structs::*; - -/// Representation of the handle point(s) in a bezier segment. -#[derive(Copy, Clone, PartialEq, Debug)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum BezierHandles { - Linear, - /// Handles for a quadratic curve. - Quadratic { - /// Point representing the location of the single handle. - handle: DVec2, - }, - /// Handles for a cubic curve. - Cubic { - /// Point representing the location of the handle associated to the start point. - handle_start: DVec2, - /// Point representing the location of the handle associated to the end point. - handle_end: DVec2, - }, -} - -impl std::hash::Hash for BezierHandles { - fn hash(&self, state: &mut H) { - std::mem::discriminant(self).hash(state); - match self { - BezierHandles::Linear => {} - BezierHandles::Quadratic { handle } => handle.to_array().map(|v| v.to_bits()).hash(state), - BezierHandles::Cubic { handle_start, handle_end } => [handle_start, handle_end].map(|handle| handle.to_array().map(|v| v.to_bits())).hash(state), - } - } -} - -impl BezierHandles { - pub fn is_cubic(&self) -> bool { - matches!(self, Self::Cubic { .. }) - } - - pub fn is_finite(&self) -> bool { - match self { - BezierHandles::Linear => true, - BezierHandles::Quadratic { handle } => handle.is_finite(), - BezierHandles::Cubic { handle_start, handle_end } => handle_start.is_finite() && handle_end.is_finite(), - } - } - - /// Get the coordinates of the bezier segment's first handle point. This represents the only handle in a quadratic segment. - pub fn start(&self) -> Option { - match *self { - BezierHandles::Cubic { handle_start, .. } | BezierHandles::Quadratic { handle: handle_start } => Some(handle_start), - _ => None, - } - } - - /// Get the coordinates of the second handle point. This will return `None` for a quadratic segment. - pub fn end(&self) -> Option { - match *self { - BezierHandles::Cubic { handle_end, .. } => Some(handle_end), - _ => None, - } - } - - pub fn move_start(&mut self, delta: DVec2) { - if let BezierHandles::Cubic { handle_start, .. } | BezierHandles::Quadratic { handle: handle_start } = self { - *handle_start += delta - } - } - - pub fn move_end(&mut self, delta: DVec2) { - if let BezierHandles::Cubic { handle_end, .. } = self { - *handle_end += delta - } - } - - /// Returns a Bezier curve that results from applying the transformation function to each handle point in the Bezier. - #[must_use] - pub fn apply_transformation(&self, transformation_function: impl Fn(DVec2) -> DVec2) -> Self { - match *self { - BezierHandles::Linear => Self::Linear, - BezierHandles::Quadratic { handle } => { - let handle = transformation_function(handle); - Self::Quadratic { handle } - } - BezierHandles::Cubic { handle_start, handle_end } => { - let handle_start = transformation_function(handle_start); - let handle_end = transformation_function(handle_end); - Self::Cubic { handle_start, handle_end } - } - } - } - - #[must_use] - pub fn reversed(self) -> Self { - match self { - BezierHandles::Cubic { handle_start, handle_end } => Self::Cubic { - handle_start: handle_end, - handle_end: handle_start, - }, - _ => self, - } - } -} - -#[cfg(feature = "dyn-any")] -unsafe impl dyn_any::StaticType for BezierHandles { - type Static = BezierHandles; -} - -/// Representation of a bezier curve with 2D points. -#[derive(Copy, Clone, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Bezier { - /// Start point of the bezier curve. - pub start: DVec2, - /// End point of the bezier curve. - pub end: DVec2, - /// Handles of the bezier curve. - pub handles: BezierHandles, -} - -impl Debug for Bezier { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - let mut debug_struct = f.debug_struct("Bezier"); - let mut debug_struct_ref = debug_struct.field("start", &self.start); - debug_struct_ref = match self.handles { - BezierHandles::Linear => debug_struct_ref, - BezierHandles::Quadratic { handle } => debug_struct_ref.field("handle", &handle), - BezierHandles::Cubic { handle_start, handle_end } => debug_struct_ref.field("handle_start", &handle_start).field("handle_end", &handle_end), - }; - debug_struct_ref.field("end", &self.end).finish() - } -} - -#[cfg(feature = "dyn-any")] -unsafe impl dyn_any::StaticType for Bezier { - type Static = Bezier; -} diff --git a/libraries/bezier-rs/src/bezier/solvers.rs b/libraries/bezier-rs/src/bezier/solvers.rs deleted file mode 100644 index f1a9a773ed..0000000000 --- a/libraries/bezier-rs/src/bezier/solvers.rs +++ /dev/null @@ -1,1175 +0,0 @@ -use super::*; -use crate::polynomial::Polynomial; -use crate::utils::{TValue, solve_cubic, solve_quadratic}; -use glam::DMat2; -use std::ops::Range; - -/// Functionality that solve for various curve information such as derivative, tangent, intersect, etc. -impl Bezier { - /// Get roots as [[x], [y]] - #[must_use] - pub fn roots(self) -> [Vec; 2] { - let (x, y) = self.parametric_polynomial(); - let x = poly_cool::Poly::new(x.coefficients().iter().copied()); - let y = poly_cool::Poly::new(y.coefficients().iter().copied()); - [x.roots_between(0., 1., 1e-8), y.roots_between(0., 1., 1e-8)] - } - - /// Returns a list of lists of points representing the De Casteljau points for all iterations at the point `t` along the curve using De Casteljau's algorithm. - /// The `i`th element of the list represents the set of points in the `i`th iteration. - /// More information on the algorithm can be found in the [De Casteljau section](https://pomax.github.io/bezierinfo/#decasteljau) in Pomax's primer. - /// - pub fn de_casteljau_points(&self, t: TValue) -> Vec> { - let t = self.t_value_to_parametric(t); - let bezier_points = match self.handles { - BezierHandles::Linear => vec![self.start, self.end], - BezierHandles::Quadratic { handle } => vec![self.start, handle, self.end], - BezierHandles::Cubic { handle_start, handle_end } => vec![self.start, handle_start, handle_end, self.end], - }; - let mut de_casteljau_points = vec![bezier_points]; - let mut current_points = de_casteljau_points.last().unwrap(); - - // Iterate until one point is left, that point will be equal to `evaluate(t)` - while current_points.len() > 1 { - // Map from every adjacent pair of points to their respective midpoints, which decrements by 1 the number of points for the next iteration - let next_points: Vec = current_points.as_slice().windows(2).map(|pair| DVec2::lerp(pair[0], pair[1], t)).collect(); - de_casteljau_points.push(next_points); - - current_points = de_casteljau_points.last().unwrap(); - } - - de_casteljau_points - } - - /// Returns two [`Polynomial`]s representing the parametric equations for x and y coordinates of the bezier curve respectively. - /// The domain of both the equations are from t=0.0 representing the start and t=1.0 representing the end of the bezier curve. - pub fn parametric_polynomial(&self) -> (Polynomial<4>, Polynomial<4>) { - match self.handles { - BezierHandles::Linear => { - let term1 = self.end - self.start; - - (Polynomial::new([self.start.x, term1.x, 0., 0.]), Polynomial::new([self.start.y, term1.y, 0., 0.])) - } - BezierHandles::Quadratic { handle } => { - let term1 = 2. * (handle - self.start); - let term2 = self.start - 2. * handle + self.end; - - (Polynomial::new([self.start.x, term1.x, term2.x, 0.]), Polynomial::new([self.start.y, term1.y, term2.y, 0.])) - } - BezierHandles::Cubic { handle_start, handle_end } => { - let term1 = 3. * (handle_start - self.start); - let term2 = 3. * (handle_end - handle_start) - term1; - let term3 = self.end - self.start - term2 - term1; - - (Polynomial::new([self.start.x, term1.x, term2.x, term3.x]), Polynomial::new([self.start.y, term1.y, term2.y, term3.y])) - } - } - } - - /// Returns a [Bezier] representing the derivative of the original curve. - /// - This function returns `None` for a linear segment. - /// - pub fn derivative(&self) -> Option { - match self.handles { - BezierHandles::Linear => None, - BezierHandles::Quadratic { handle } => { - let p1_minus_p0 = handle - self.start; - let p2_minus_p1 = self.end - handle; - Some(Bezier::from_linear_dvec2(2. * p1_minus_p0, 2. * p2_minus_p1)) - } - BezierHandles::Cubic { handle_start, handle_end } => { - let p1_minus_p0 = handle_start - self.start; - let p2_minus_p1 = handle_end - handle_start; - let p3_minus_p2 = self.end - handle_end; - Some(Bezier::from_quadratic_dvec2(3. * p1_minus_p0, 3. * p2_minus_p1, 3. * p3_minus_p2)) - } - } - } - - /// Returns the non-normalized vector representing the tangent at the point `t` along the curve. - pub(crate) fn non_normalized_tangent(&self, t: f64) -> DVec2 { - match self.handles { - BezierHandles::Linear => self.end - self.start, - _ => self.derivative().unwrap().evaluate(TValue::Parametric(t)), - } - } - - /// Returns a normalized unit vector representing the tangent at the point `t` along the curve. - /// - pub fn tangent(&self, t: TValue) -> DVec2 { - let t = self.t_value_to_parametric(t); - let tangent = self.non_normalized_tangent(t); - if tangent.length() > 0. { tangent.normalize() } else { tangent } - } - - /// Find the `t`-value(s) such that the tangent(s) at `t` pass through the specified point. - /// - #[must_use] - pub fn tangents_to_point(self, point: DVec2) -> Vec { - // We solve deriv(t) * (self(t) - point) = 0. In principle, this is a quintic. - // In fact, the highest-order term cancels out so it's at most a quartic. - let (mut x, mut y) = self.parametric_polynomial(); - let x = x.coefficients_mut(); - let y = y.coefficients_mut(); - x[0] -= point.x; - y[0] -= point.y; - let poly = poly_cool::Poly::new([ - x[0] * y[1] - y[0] * x[1], - 2.0 * (x[0] * y[2] - y[0] * x[2]), - x[2] * y[1] - y[2] * x[1] + 2.0 * (x[1] * y[2] - y[1] * x[2]) + 3.0 * (x[0] * y[3] - y[0] * x[3]), - x[3] * y[1] - y[3] * x[1] + 3.0 * (x[1] * y[3] - y[1] * x[3]), - 2.0 * (x[3] * y[2] - y[3] * x[2]) + 3.0 * (x[2] * y[3] - y[2] * x[3]), - ]); - poly.roots_between(0.0, 1.0, 1e-8) - } - - /// Returns a normalized unit vector representing the direction of the normal at the point `t` along the curve. - /// - pub fn normal(&self, t: TValue) -> DVec2 { - self.tangent(t).perp() - } - - /// Find the `t`-value(s) such that the normal(s) at `t` pass through the specified point. - /// - #[must_use] - pub fn normals_to_point(self, point: DVec2) -> Vec { - // We solve deriv(t) dot (self(t) - point) = 0. - let (mut x, mut y) = self.parametric_polynomial(); - let x = x.coefficients_mut(); - let y = y.coefficients_mut(); - x[0] -= point.x; - y[0] -= point.y; - let poly = poly_cool::Poly::new([ - x[0] * x[1] + y[0] * y[1], - x[1] * x[1] + y[1] * y[1] + 2. * (x[0] * x[2] + y[0] * y[2]), - 3. * (x[2] * x[1] + y[2] * y[1]) + 3. * (x[0] * x[3] + y[0] * y[3]), - 4. * (x[3] * x[1] + y[3] * y[1]) + 2. * (x[2] * x[2] + y[2] * y[2]), - 5. * (x[3] * x[2] + y[3] * y[2]), - 3. * (x[3] * x[3] + y[3] * y[3]), - ]); - poly.roots_between(0., 1., 1e-8) - } - - /// Returns the curvature, a scalar value for the derivative at the point `t` along the curve. - /// Curvature is 1 over the radius of a circle with an equivalent derivative. - /// - pub fn curvature(&self, t: TValue) -> f64 { - let t = self.t_value_to_parametric(t); - let (d, dd) = match &self.derivative() { - Some(first_derivative) => match first_derivative.derivative() { - Some(second_derivative) => (first_derivative.evaluate(TValue::Parametric(t)), second_derivative.evaluate(TValue::Parametric(t))), - None => (first_derivative.evaluate(TValue::Parametric(t)), first_derivative.end - first_derivative.start), - }, - None => (self.end - self.start, DVec2::new(0., 0.)), - }; - - let numerator = d.x * dd.y - d.y * dd.x; - let denominator = (d.x.powf(2.) + d.y.powf(2.)).powf(1.5); - if denominator.abs() < MAX_ABSOLUTE_DIFFERENCE { 0. } else { numerator / denominator } - } - - /// Returns two lists of `t`-values representing the local extrema of the `x` and `y` parametric curves respectively. - /// The local extrema are defined to be points at which the derivative of the curve is equal to zero. - fn unrestricted_local_extrema(&self) -> [[Option; 3]; 2] { - match self.handles { - BezierHandles::Linear => [[None; 3]; 2], - BezierHandles::Quadratic { handle } => { - let d0 = handle - self.start; - let d1 = self.end - handle; - let dd = d1 - d0; - let a = (dd.x != 0.).then(|| -d0.x / dd.x); - let b = (dd.y != 0.).then(|| -d0.y / dd.y); - [[a, None, None], [b, None, None]] - } - BezierHandles::Cubic { handle_start, handle_end } => { - let d0 = handle_start - self.start; - let d1 = handle_end - handle_start; - let d2 = self.end - handle_end; - let a = d0 - 2. * d1 + d2; - let b = 2. * (d1 - d0); - let c = d0; - let discriminant = b * b - 4. * a * c; - let two_times_a = 2. * a; - [ - utils::solve_quadratic(discriminant.x, two_times_a.x, b.x, c.x), - utils::solve_quadratic(discriminant.y, two_times_a.y, b.y, c.y), - ] - } - } - } - - /// Returns two lists of `t`-values representing the local extrema of the `x` and `y` parametric curves respectively. - /// The list of `t`-values returned are filtered such that they fall within the range `[0, 1]`. - /// - pub fn local_extrema(&self) -> [impl Iterator; 2] { - self.unrestricted_local_extrema().map(|t_values| t_values.into_iter().flatten().filter(|&t| t > 0. && t < 1.)) - } - - /// Return the min and max corners that represent the bounding box of the curve. - /// - pub fn bounding_box(&self) -> [DVec2; 2] { - // Start by taking min/max of endpoints. - let mut endpoints_min = self.start.min(self.end); - let mut endpoints_max = self.start.max(self.end); - - // Iterate through extrema points. - let extrema = self.local_extrema(); - for t_values in extrema { - for t in t_values { - let point = self.evaluate(TValue::Parametric(t)); - // Update bounding box if new min/max is found. - endpoints_min = endpoints_min.min(point); - endpoints_max = endpoints_max.max(point); - } - } - - [endpoints_min, endpoints_max] - } - - /// Return the min and max corners that represent the bounding box enclosing this Bezier's two anchor points and any handles. - pub fn bounding_box_of_anchors_and_handles(&self) -> [DVec2; 2] { - match self.handles { - BezierHandles::Linear => [self.start.min(self.end), self.start.max(self.end)], - BezierHandles::Quadratic { handle } => [self.start.min(self.end).min(handle), self.start.max(self.end).max(handle)], - BezierHandles::Cubic { handle_start, handle_end } => [self.start.min(self.end).min(handle_start).min(handle_end), self.start.max(self.end).max(handle_start).max(handle_end)], - } - } - - /// Returns `true` if the bounding box of the bezier is contained entirely within a rectangle defined by its minimum and maximum corners. - pub fn is_contained_within(&self, min_corner: DVec2, max_corner: DVec2) -> bool { - let [bounding_box_min, bounding_box_max] = self.bounding_box(); - min_corner.x <= bounding_box_min.x && min_corner.y <= bounding_box_min.y && bounding_box_max.x <= max_corner.x && bounding_box_max.y <= max_corner.y - } - - /// Returns an `Iterator` containing all possible parametric `t`-values at the given `x`-coordinate. - pub fn find_tvalues_for_x(&self, x: f64) -> impl Iterator + use<> { - // Compute the roots of the resulting bezier curve - match self.handles { - BezierHandles::Linear => { - // If the transformed linear bezier is on the x-axis, `a` and `b` will both be zero and `solve_linear` will return no roots - let a = self.end.x - self.start.x; - let b = self.start.x - x; - utils::solve_linear(a, b) - } - BezierHandles::Quadratic { handle } => { - let a = self.start.x - 2. * handle.x + self.end.x; - let b = 2. * (handle.x - self.start.x); - let c = self.start.x - x; - - let discriminant = b * b - 4. * a * c; - let two_times_a = 2. * a; - - utils::solve_quadratic(discriminant, two_times_a, b, c) - } - BezierHandles::Cubic { handle_start, handle_end } => { - let start_x = self.start.x; - let a = -start_x + 3. * handle_start.x - 3. * handle_end.x + self.end.x; - let b = 3. * start_x - 6. * handle_start.x + 3. * handle_end.x; - let c = -3. * start_x + 3. * handle_start.x; - let d = start_x - x; - - utils::solve_cubic(a, b, c, d) - } - } - .into_iter() - .flatten() - .filter(|&t| utils::f64_approximately_in_range(t, 0., 1., MAX_ABSOLUTE_DIFFERENCE)) - } - - // TODO: Use an `impl Iterator` return type instead of a `Vec` - /// Returns list of `t`-values representing the inflection points of the curve. - /// The inflection points are defined to be points at which the second derivative of the curve is equal to zero. - pub fn unrestricted_inflections(&self) -> impl Iterator { - match self.handles { - // There exists no inflection points for linear and quadratic beziers. - BezierHandles::Linear => [None; 3], - BezierHandles::Quadratic { .. } => [None; 3], - BezierHandles::Cubic { .. } => { - // Axis align the curve. - let translated_bezier = self.translate(-self.start); - let angle = translated_bezier.end.angle_to(DVec2::new(1., 0.)); - let rotated_bezier = translated_bezier.rotate(angle); - if let BezierHandles::Cubic { handle_start, handle_end } = rotated_bezier.handles { - // These formulas and naming conventions follows https://pomax.github.io/bezierinfo/#inflections - let a = handle_end.x * handle_start.y; - let b = rotated_bezier.end.x * handle_start.y; - let c = handle_start.x * handle_end.y; - let d = rotated_bezier.end.x * handle_end.y; - - let x = -3. * a + 2. * b + 3. * c - d; - let y = 3. * a - b - 3. * c; - let z = c - a; - - let discriminant = y * y - 4. * x * z; - utils::solve_quadratic(discriminant, 2. * x, y, z) - } else { - unreachable!("shouldn't happen") - } - } - } - .into_iter() - .flatten() - } - - /// Returns list of parametric `t`-values representing the inflection points of the curve. - /// The list of `t`-values returned are filtered such that they fall within the range `[0, 1]`. - /// - pub fn inflections(&self) -> Vec { - self.unrestricted_inflections().filter(|&t| t > 0. && t < 1.).collect::>() - } - - /// Implementation of the algorithm to find curve intersections by iterating on bounding boxes. - /// - `self_original_t_interval` - Used to identify the `t` values of the original parent of `self` that the current iteration is representing. - /// - `other_original_t_interval` - Used to identify the `t` values of the original parent of `other` that the current iteration is representing. - pub(crate) fn intersections_between_subcurves(&self, self_original_t_interval: Range, other: &Bezier, other_original_t_interval: Range, error: f64) -> Vec<[f64; 2]> { - let bounding_box1 = self.bounding_box(); - let bounding_box2 = other.bounding_box(); - - // Get the `t` interval of the original parent of `self` and determine the middle `t` value - let Range { start: self_start_t, end: self_end_t } = self_original_t_interval; - let self_mid_t = (self_start_t + self_end_t) / 2.; - - // Get the `t` interval of the original parent of `other` and determine the middle `t` value - let Range { - start: other_start_t, - end: other_end_t, - } = other_original_t_interval; - let other_mid_t = (other_start_t + other_end_t) / 2.; - - let error_threshold = DVec2::new(error, error); - - // Check if the bounding boxes overlap - if utils::do_rectangles_overlap(bounding_box1, bounding_box2) { - // If bounding boxes are within the error threshold (i.e. are small enough), we have found an intersection - if (bounding_box1[1] - bounding_box1[0]).cmplt(error_threshold).all() && (bounding_box2[1] - bounding_box2[0]).cmplt(error_threshold).all() { - // Use the middle t value, return the corresponding `t` value for `self` and `other` - return vec![[self_mid_t, other_mid_t]]; - } - - // Split curves in half and repeat with the combinations of the two halves of each curve - let [split_1_a, split_1_b] = self.split(TValue::Parametric(0.5)); - let [split_2_a, split_2_b] = other.split(TValue::Parametric(0.5)); - - [ - split_1_a.intersections_between_subcurves(self_start_t..self_mid_t, &split_2_a, other_start_t..other_mid_t, error), - split_1_a.intersections_between_subcurves(self_start_t..self_mid_t, &split_2_b, other_mid_t..other_end_t, error), - split_1_b.intersections_between_subcurves(self_mid_t..self_end_t, &split_2_a, other_start_t..other_mid_t, error), - split_1_b.intersections_between_subcurves(self_mid_t..self_end_t, &split_2_b, other_mid_t..other_end_t, error), - ] - .concat() - } else { - vec![] - } - } - - // TODO: Use an `impl Iterator` return type instead of a `Vec` - /// Returns a list of filtered parametric `t` values that correspond to intersection points between the current bezier curve and the provided one - /// such that the difference between adjacent `t` values in sorted order is greater than some minimum separation value. If the difference - /// between 2 adjacent `t` values is less than the minimum difference, the filtering takes the larger `t` value and discards the smaller `t` value. - /// The returned `t` values are with respect to the current bezier, not the provided parameter. - /// If the provided curve is linear, then zero intersection points will be returned along colinear segments. - /// - `error` - For intersections where the provided bezier is non-linear, `error` defines the threshold for bounding boxes to be considered an intersection point. - /// - `minimum_separation` - The minimum difference between adjacent `t` values in sorted order - /// - pub fn intersections(&self, other: &Bezier, error: Option, minimum_separation: Option) -> Vec { - // TODO: Consider using the `intersections_between_vectors_of_curves` helper function here - // Otherwise, use bounding box to determine intersections - let mut intersection_t_values = self.unfiltered_intersections(other, error); - intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap()); - - intersection_t_values.iter().map(|x| x[0]).fold(Vec::new(), |mut accumulator, t| { - if !accumulator.is_empty() && (accumulator.last().unwrap() - t).abs() < minimum_separation.unwrap_or(MIN_SEPARATION_VALUE) { - accumulator.pop(); - } - accumulator.push(t); - accumulator - }) - } - - // TODO: Use an `impl Iterator` return type instead of a `Vec` - /// Returns a list of pairs of filtered parametric `t` values that correspond to intersection points between the current bezier curve and the provided one - /// such that the difference between adjacent `t` values in sorted order is greater than some minimum separation value. If the difference - /// between 2 adjacent `t` values is less than the minimum difference, the filtering takes the larger `t` value and discards the smaller `t` value. - /// The first value in pair is with respect to the current bezier and the second value in pair is with respect to the provided parameter. - /// If the provided curve is linear, then zero intersection points will be returned along colinear segments. - /// - `error` - For intersections where the provided bezier is non-linear, `error` defines the threshold for bounding boxes to be considered an intersection point. - /// - `minimum_separation` - The minimum difference between adjacent `t` values in sorted order - pub fn all_intersections(&self, other: &Bezier, error: Option, minimum_separation: Option) -> Vec<[f64; 2]> { - // TODO: Consider using the `intersections_between_vectors_of_curves` helper function here - // Otherwise, use bounding box to determine intersections - let mut intersection_t_values = self.unfiltered_intersections(other, error); - intersection_t_values.sort_by(|a, b| (a[0] + a[1]).partial_cmp(&(b[0] + b[1])).unwrap()); - - intersection_t_values.iter().fold(Vec::new(), |mut accumulator, t| { - if !accumulator.is_empty() - && (accumulator.last().unwrap()[0] - t[0]).abs() < minimum_separation.unwrap_or(MIN_SEPARATION_VALUE) - && (accumulator.last().unwrap()[1] - t[1]).abs() < minimum_separation.unwrap_or(MIN_SEPARATION_VALUE) - { - accumulator.pop(); - } - accumulator.push(*t); - accumulator - }) - } - - // TODO: Use an `impl Iterator` return type instead of a `Vec` - /// Returns a list of `t` values that correspond to intersection points between the current bezier curve and the provided one. The returned `t` values are with respect to the current bezier, not the provided parameter. - /// If the provided curve is linear, then zero intersection points will be returned along colinear segments. - /// - `error` - For intersections where the provided bezier is non-linear, `error` defines the threshold for bounding boxes to be considered an intersection point. - pub fn unfiltered_intersections(&self, other: &Bezier, error: Option) -> Vec<[f64; 2]> { - let error = error.unwrap_or(0.5); - - // TODO: This implementation does not handle the case of line-like bezier curves properly. Two line-like bezier curves which have the same slope - // should not return any intersection points but the current implementation returns many of them. This results in the area of line not being zero. - // Using `is_linear` does prevent it but only in cases where the line-like cubic bezier has it handles at exactly the same position as the start - // and end points. In future, the below algorithm needs to be changed to account for all possible cases. - - if other.is_linear() { - // Rotate the bezier and the line by the angle that the line makes with the x axis - let line_directional_vector = other.end - other.start; - let angle = line_directional_vector.angle_to(DVec2::new(0., 1.)); - let rotation_matrix = DMat2::from_angle(angle); - let rotated_bezier = self.apply_transformation(|point| rotation_matrix * point); - - // Translate the bezier such that the line becomes aligned on top of the x-axis - let vertical_distance = (rotation_matrix * other.start).x; - let translated_bezier = rotated_bezier.translate(DVec2::new(-vertical_distance, 0.)); - - let y_start = (rotation_matrix * other.start).y; - let y_end = (rotation_matrix * other.end).y; - - // Compute the roots of the resulting bezier curve - let list_intersection_t = translated_bezier.find_tvalues_for_x(0.); - - // Calculate line's bounding box - let [min_corner, max_corner] = other.bounding_box_of_anchors_and_handles(); - - return list_intersection_t - // Accept the t value if it is approximately in [0, 1] and if the corresponding coordinates are within the range of the linear line - .filter(|&t| utils::dvec2_approximately_in_range(self.unrestricted_parametric_evaluate(t), min_corner, max_corner, MAX_ABSOLUTE_DIFFERENCE).all()) - // Ensure the returned value is within the correct range - .map(|t| t.clamp(0., 1.)) - .map(|t| { - let y = translated_bezier.evaluate(TValue::Parametric(t)).y; - let other_t = (y-y_start)/(y_end-y_start); - [t, other_t] - }) - .collect::>(); - } - - // TODO: Consider using the `intersections_between_vectors_of_curves` helper function here - // Otherwise, use bounding box to determine intersections - self.intersections_between_subcurves(0. ..1., other, 0. ..1., error).to_vec() - } - - /// Returns a list of `t` values that correspond to points on this Bezier segment where they intersect with the given line. (`direction_vector` does not need to be normalized.) - /// If this needs to be called frequently with a line of the same rotation angle, consider instead using [`line_test_crossings_prerotated`] and moving this function's setup code into your own logic before the repeated call. - pub fn line_test_crossings(&self, point_on_line: DVec2, direction_vector: DVec2) -> impl Iterator + '_ { - // Rotate the bezier and the line by the angle that the line makes with the x axis - let angle = direction_vector.angle_to(DVec2::new(0., 1.)); - let rotation_matrix = DMat2::from_angle(angle); - let rotated_bezier = self.apply_transformation(|point| rotation_matrix * point); - - self.line_test_crossings_prerotated(point_on_line, rotation_matrix, rotated_bezier) - } - - /// Returns a list of `t` values that correspond to points on this Bezier segment where they intersect with the given infinite line. - /// This version of the function is for better performance when calling it frequently without needing to change the rotation between each call. - /// If that isn't important, use [`line_test_crossings`] which wraps this and provides an easier interface by taking a line rotation vector. - /// Instead, this version requires a rotation matrix for the line's rotation and a version of this Bezier segment that has had its rotation already applied. - pub fn line_test_crossings_prerotated(&self, point_on_line: DVec2, rotation_matrix: DMat2, rotated_bezier: Self) -> impl Iterator + '_ { - // Translate the bezier such that the line becomes aligned on top of the x-axis - let vertical_distance = (rotation_matrix.x_axis.x * point_on_line.x) + (rotation_matrix.y_axis.x * point_on_line.y); - let translated_bezier = rotated_bezier.translate(DVec2::new(-vertical_distance, 0.)); - - // Compute the roots of the resulting bezier curve - translated_bezier.find_tvalues_for_x(0.) - } - - /// Returns a list of `t` values that correspond to points on this Bezier segment where they intersect with the given ray. (`ray_direction` does not need to be normalized.) - /// If this needs to be called frequently with a ray of the same rotation angle, consider instead using [`ray_test_crossings_prerotated`] and moving this function's setup code into your own logic before the repeated call. - pub fn ray_test_crossings(&self, ray_start: DVec2, ray_direction: DVec2) -> impl Iterator + '_ { - // Rotate the bezier and the line by the angle that the line makes with the x axis - let angle = ray_direction.angle_to(DVec2::new(0., 1.)); - let rotation_matrix = DMat2::from_angle(angle); - let rotated_bezier = self.apply_transformation(|point| rotation_matrix * point); - - self.ray_test_crossings_prerotated(ray_start, rotation_matrix, rotated_bezier) - } - - /// Returns a list of `t` values that correspond to points on this Bezier segment where they intersect with the given infinite ray. - /// This version of the function is for better performance when calling it frequently without needing to change the rotation between each call. - /// If that isn't important, use [`ray_test_crossings`] which wraps this and provides an easier interface by taking a ray direction vector. - /// Instead, this version requires a rotation matrix for the ray's rotation and a version of this Bezier segment that has had its rotation already applied. - pub fn ray_test_crossings_prerotated(&self, ray_start: DVec2, rotation_matrix: DMat2, rotated_bezier: Self) -> impl Iterator + '_ { - // Intersection t-values include those beyond the [0-1] range where the segment's ends extend through the X-axis - let intersection_t_values_on_rotated_bezier = self.line_test_crossings_prerotated(ray_start, rotation_matrix, rotated_bezier); - - intersection_t_values_on_rotated_bezier - // Accept the t value if it is approximately in [0, 1] and if the corresponding coordinates are within the range of the linear line - .filter(move |&t| { - let point = self.unrestricted_parametric_evaluate(t); - // Ensure the returned value is within the correct range - let in_bounds = point.cmpge(ray_start) | utils::dvec2_compare(point, ray_start, MAX_ABSOLUTE_DIFFERENCE); - in_bounds.x && in_bounds.y - }) - } - - /// Helper function to compute intersections between lists of subcurves. - /// This function uses the algorithm implemented in `intersections_between_subcurves`. - fn intersections_between_vectors_of_curves(subcurves1: &[(Bezier, Range)], subcurves2: &[(Bezier, Range)], error: f64) -> Vec<[f64; 2]> { - let segment_pairs = subcurves1.iter().flat_map(move |(curve1, curve1_t_pair)| { - subcurves2 - .iter() - .filter_map(move |(curve2, curve2_t_pair)| utils::do_rectangles_overlap(curve1.bounding_box(), curve2.bounding_box()).then_some((curve1, curve1_t_pair, curve2, curve2_t_pair))) - }); - segment_pairs - .flat_map(|(curve1, curve1_t_pair, curve2, curve2_t_pair)| curve1.intersections_between_subcurves(curve1_t_pair.clone(), curve2, curve2_t_pair.clone(), error)) - .collect::>() - } - - // TODO: Use an `impl Iterator` return type instead of a `Vec` - /// Returns a list of parametric `t` values that correspond to the self intersection points of the current bezier curve. For each intersection point, the returned `t` value is the smaller of the two that correspond to the point. - /// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point. - /// - fn unfiltered_self_intersections(&self, error: Option) -> Vec<[f64; 2]> { - if self.handles == BezierHandles::Linear || matches!(self.handles, BezierHandles::Quadratic { .. }) { - return vec![]; - } - - let error = error.unwrap_or(0.5); - - // Get 2 copies of the reduced curves - let (self1, self1_t_values) = self.reduced_curves_and_t_values(None); - let (self2, self2_t_values) = (self1.clone(), self1_t_values.clone()); - let num_curves = self1.len(); - - // Adjacent reduced curves cannot intersect - if num_curves <= 2 { - return vec![]; - } - - // Create iterators that combine a subcurve with the `t` value pair that it was trimmed with - let combined_iterator1 = self1.into_iter().zip(self1_t_values.iter().map(|t_pair| Range { start: t_pair[0], end: t_pair[1] })); - // Second one needs to be a list because Iterator does not implement copy - let combined_list2: Vec<(Bezier, Range)> = self2.into_iter().zip(self2_t_values.iter().map(|t_pair| Range { start: t_pair[0], end: t_pair[1] })).collect(); - - // For each curve, look for intersections with every curve that is at least 2 indices away - combined_iterator1 - .take(num_curves - 2) - .enumerate() - .flat_map(|(index, (subcurve, t_pair))| Bezier::intersections_between_vectors_of_curves(&[(subcurve, t_pair)], &combined_list2[index + 2..], error)) - .collect() - } - - // TODO: Use an `impl Iterator` return type instead of a `Vec` - /// Returns a list of parametric `t` values that correspond to the self intersection points of the current bezier curve. For each intersection point, the returned `t` value is the smaller of the two that correspond to the point. - /// If the difference between 2 adjacent `t` values is less than the minimum difference, the filtering takes the larger `t` value and discards the smaller `t` value. - /// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point. - /// - `minimum_separation` - The minimum difference between adjacent `t` values in sorted order - pub fn self_intersections(&self, error: Option, minimum_separation: Option) -> Vec<[f64; 2]> { - let mut intersection_t_values = self.unfiltered_self_intersections(error); - intersection_t_values.sort_by(|a, b| (a[0] + a[1]).partial_cmp(&(b[0] + b[1])).unwrap()); - - intersection_t_values.iter().fold(Vec::new(), |mut accumulator, t| { - if !accumulator.is_empty() - && (accumulator.last().unwrap()[0] - t[0]).abs() < minimum_separation.unwrap_or(MIN_SEPARATION_VALUE) - && (accumulator.last().unwrap()[1] - t[1]).abs() < minimum_separation.unwrap_or(MIN_SEPARATION_VALUE) - { - accumulator.pop(); - } - accumulator.push(*t); - accumulator - }) - } - - /// Returns a list of parametric `t` values that correspond to the intersection points between the curve and a rectangle defined by opposite corners. - /// - pub fn rectangle_intersections(&self, corner1: DVec2, corner2: DVec2) -> Vec { - [ - Bezier::from_linear_coordinates(corner1.x, corner1.y, corner2.x, corner1.y), - Bezier::from_linear_coordinates(corner2.x, corner1.y, corner2.x, corner2.y), - Bezier::from_linear_coordinates(corner2.x, corner2.y, corner1.x, corner2.y), - Bezier::from_linear_coordinates(corner1.x, corner2.y, corner1.x, corner1.y), - ] - .iter() - .flat_map(|bezier| self.intersections(bezier, None, None)) - .collect() - } - - /// Returns a cubic bezier which joins this with the provided bezier curve. - /// The resulting path formed by the Bezier curves is continuous up to the first derivative. - /// - pub fn join(&self, other: &Bezier) -> Bezier { - let handle1 = self.non_normalized_tangent(1.) / 3. + self.end; - let handle2 = other.start - other.non_normalized_tangent(0.) / 3.; - Bezier::from_cubic_dvec2(self.end, handle1, handle2, other.start) - } - - /// Compute the winding order (number of times crossing an infinite line to the left of the point) - /// - /// Assumes curve is split at the extrema. - fn pre_split_winding_number(&self, target_point: DVec2) -> i32 { - // Clockwise is -1, anticlockwise is +1 (with +y as up) - // Looking only to the left (-x) of the target_point - let resulting_sign = if self.end.y > self.start.y { - if target_point.y < self.start.y || target_point.y >= self.end.y { - return 0; - } - -1 - } else if self.end.y < self.start.y { - if target_point.y < self.end.y || target_point.y >= self.start.y { - return 0; - } - 1 - } else { - return 0; - }; - match &self.handles { - BezierHandles::Linear => { - if target_point.x < self.start.x.min(self.end.x) { - return 0; - } - if target_point.x >= self.start.x.max(self.end.x) { - return resulting_sign; - } - // line equation ax + by = c - let a = self.end.y - self.start.y; - let b = self.start.x - self.end.x; - let c = a * self.start.x + b * self.start.y; - if (a * target_point.x + b * target_point.y - c) * (resulting_sign as f64) <= 0. { - resulting_sign - } else { - 0 - } - } - BezierHandles::Quadratic { handle: p1 } => { - if target_point.x < self.start.x.min(self.end.x).min(p1.x) { - return 0; - } - if target_point.x >= self.start.x.max(self.end.x).max(p1.x) { - return resulting_sign; - } - let a = self.end.y - 2. * p1.y + self.start.y; - let b = 2. * (p1.y - self.start.y); - let c = self.start.y - target_point.y; - - let discriminant = b * b - 4. * a * c; - let two_times_a = 2. * a; - for t in solve_quadratic(discriminant, two_times_a, b, c).into_iter().flatten() { - if (0.0..=1.).contains(&t) { - let x = self.evaluate(TValue::Parametric(t)).x; - if target_point.x >= x { - return resulting_sign; - } else { - return 0; - } - } - } - 0 - } - BezierHandles::Cubic { handle_start: p1, handle_end: p2 } => { - if target_point.x < self.start.x.min(self.end.x).min(p1.x).min(p2.x) { - return 0; - } - if target_point.x >= self.start.x.max(self.end.x).max(p1.x).max(p2.x) { - return resulting_sign; - } - let a = self.end.y - 3. * p2.y + 3. * p1.y - self.start.y; - let b = 3. * (p2.y - 2. * p1.y + self.start.y); - let c = 3. * (p1.y - self.start.y); - let d = self.start.y - target_point.y; - for t in solve_cubic(a, b, c, d).into_iter().flatten() { - if (0.0..=1.).contains(&t) { - let x = self.evaluate(TValue::Parametric(t)).x; - if target_point.x >= x { - return resulting_sign; - } else { - return 0; - } - } - } - 0 - } - } - } - - /// Compute the winding number contribution of a single segment. - /// - /// Cast a ray to the left and count intersections. - pub fn winding(&self, target_point: DVec2) -> i32 { - let [x_extrema_t, y_extrema_t] = self.unrestricted_local_extrema(); - let mut x_extrema_t = x_extrema_t.map(|t| t.filter(|&t| t > 0. && t < 1.)); - let mut y_extrema_t = y_extrema_t.map(|t| t.filter(|&t| t > 0. && t < 1.)); - - let mut results = [None; 8]; - results[7] = Some(1.); - for i in (0..7).rev() { - let Some(min) = x_extrema_t.iter_mut().chain(y_extrema_t.iter_mut()).max_by(|a, b| a.partial_cmp(b).unwrap()) else { - results[i] = Some(0.); - break; - }; - if let Some(value) = min.take() { - results[i] = Some(value); - } else { - results[i] = Some(0.); - break; - } - } - results - .windows(2) - .flat_map(|t| t[0].and_then(|first| t[1].map(|second| [first, second]))) - .map(|t| self.trim(TValue::Parametric(t[0]), TValue::Parametric(t[1])).pre_split_winding_number(target_point)) - .sum() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::compare::{compare_f64s, compare_points, compare_vec_of_points}; - - #[test] - fn test_de_casteljau_points() { - let bezier = Bezier::from_cubic_coordinates(0., 0., 0., 100., 100., 100., 100., 0.); - let de_casteljau_points = bezier.de_casteljau_points(TValue::Parametric(0.5)); - let expected_de_casteljau_points = vec![ - vec![DVec2::new(0., 0.), DVec2::new(0., 100.), DVec2::new(100., 100.), DVec2::new(100., 0.)], - vec![DVec2::new(0., 50.), DVec2::new(50., 100.), DVec2::new(100., 50.)], - vec![DVec2::new(25., 75.), DVec2::new(75., 75.)], - vec![DVec2::new(50., 75.)], - ]; - assert_eq!(&de_casteljau_points, &expected_de_casteljau_points); - - assert_eq!(expected_de_casteljau_points[3][0], bezier.evaluate(TValue::Parametric(0.5))); - } - - #[test] - fn test_derivative() { - // Test derivatives of each Bezier curve type - let p1 = DVec2::new(10., 10.); - let p2 = DVec2::new(40., 30.); - let p3 = DVec2::new(60., 60.); - let p4 = DVec2::new(70., 100.); - - let linear = Bezier::from_linear_dvec2(p1, p2); - assert!(linear.derivative().is_none()); - - let quadratic = Bezier::from_quadratic_dvec2(p1, p2, p3); - let derivative_quadratic = quadratic.derivative().unwrap(); - assert_eq!(derivative_quadratic, Bezier::from_linear_coordinates(60., 40., 40., 60.)); - - let cubic = Bezier::from_cubic_dvec2(p1, p2, p3, p4); - let derivative_cubic = cubic.derivative().unwrap(); - assert_eq!(derivative_cubic, Bezier::from_quadratic_coordinates(90., 60., 60., 90., 30., 120.)); - - // Cases where the all manipulator points are the same - let quadratic_point = Bezier::from_quadratic_dvec2(p1, p1, p1); - assert_eq!(quadratic_point.derivative().unwrap(), Bezier::from_linear_dvec2(DVec2::ZERO, DVec2::ZERO)); - - let cubic_point = Bezier::from_cubic_dvec2(p1, p1, p1, p1); - assert_eq!(cubic_point.derivative().unwrap(), Bezier::from_quadratic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::ZERO)); - } - - #[test] - fn test_tangent() { - // Test tangents at start and end points of each Bezier curve type - let p1 = DVec2::new(10., 10.); - let p2 = DVec2::new(40., 30.); - let p3 = DVec2::new(60., 60.); - let p4 = DVec2::new(70., 100.); - - let linear = Bezier::from_linear_dvec2(p1, p2); - let unit_slope = DVec2::new(30., 20.).normalize(); - assert_eq!(linear.tangent(TValue::Parametric(0.)), unit_slope); - assert_eq!(linear.tangent(TValue::Parametric(1.)), unit_slope); - - let quadratic = Bezier::from_quadratic_dvec2(p1, p2, p3); - assert_eq!(quadratic.tangent(TValue::Parametric(0.)), DVec2::new(60., 40.).normalize()); - assert_eq!(quadratic.tangent(TValue::Parametric(1.)), DVec2::new(40., 60.).normalize()); - - let cubic = Bezier::from_cubic_dvec2(p1, p2, p3, p4); - assert_eq!(cubic.tangent(TValue::Parametric(0.)), DVec2::new(90., 60.).normalize()); - assert_eq!(cubic.tangent(TValue::Parametric(1.)), DVec2::new(30., 120.).normalize()); - } - - #[test] - fn tangent_at_point() { - let validate = |bz: Bezier, p: DVec2| { - let solutions = bz.tangents_to_point(p); - assert_ne!(solutions.len(), 0); - for t in solutions { - let pos = bz.evaluate(TValue::Parametric(t)); - let expected_tangent = (pos - p).normalize(); - let tangent = bz.tangent(TValue::Parametric(t)); - assert!(expected_tangent.perp_dot(tangent).abs() < 0.2, "Expected tangent {expected_tangent} found {tangent} pos {pos}") - } - }; - let bz = Bezier::from_quadratic_coordinates(55., 50., 165., 30., 185., 170.); - let p = DVec2::new(193., 83.); - validate(bz, p); - - let bz = Bezier::from_cubic_coordinates(55., 30., 18., 139., 175., 30., 185., 160.); - let p = DVec2::new(127., 121.); - validate(bz, p); - } - - #[test] - fn test_normal() { - // Test normals at start and end points of each Bezier curve type - let p1 = DVec2::new(10., 10.); - let p2 = DVec2::new(40., 30.); - let p3 = DVec2::new(60., 60.); - let p4 = DVec2::new(70., 100.); - - let linear = Bezier::from_linear_dvec2(p1, p2); - let unit_slope = DVec2::new(-20., 30.).normalize(); - assert_eq!(linear.normal(TValue::Parametric(0.)), unit_slope); - assert_eq!(linear.normal(TValue::Parametric(1.)), unit_slope); - - let quadratic = Bezier::from_quadratic_dvec2(p1, p2, p3); - assert_eq!(quadratic.normal(TValue::Parametric(0.)), DVec2::new(-40., 60.).normalize()); - assert_eq!(quadratic.normal(TValue::Parametric(1.)), DVec2::new(-60., 40.).normalize()); - - let cubic = Bezier::from_cubic_dvec2(p1, p2, p3, p4); - assert_eq!(cubic.normal(TValue::Parametric(0.)), DVec2::new(-60., 90.).normalize()); - assert_eq!(cubic.normal(TValue::Parametric(1.)), DVec2::new(-120., 30.).normalize()); - } - - #[test] - fn normal_at_point() { - let validate = |bz: Bezier, p: DVec2| { - let solutions = bz.normals_to_point(p); - assert_ne!(solutions.len(), 0); - for t in solutions { - let pos = bz.evaluate(TValue::Parametric(t)); - let expected_normal = (pos - p).normalize(); - let normal = bz.normal(TValue::Parametric(t)); - assert!(expected_normal.perp_dot(normal).abs() < 0.2, "Expected normal {expected_normal} found {normal} pos {pos}") - } - }; - - let bz = Bezier::from_linear_coordinates(50., 50., 100., 100.); - let p = DVec2::new(100., 50.); - validate(bz, p); - - let bz = Bezier::from_quadratic_coordinates(55., 50., 165., 30., 185., 170.); - let p = DVec2::new(193., 83.); - validate(bz, p); - - let bz = Bezier::from_cubic_coordinates(55., 30., 18., 139., 175., 30., 185., 160.); - let p = DVec2::new(127., 121.); - validate(bz, p); - - let bz = Bezier::from_cubic_coordinates(55., 30., 85., 140., 175., 30., 185., 160.); - let p = DVec2::new(17., 172.); - validate(bz, p); - } - - #[test] - fn test_curvature() { - let p1 = DVec2::new(10., 10.); - let p2 = DVec2::new(50., 10.); - let p3 = DVec2::new(50., 50.); - let p4 = DVec2::new(50., 10.); - - let linear = Bezier::from_linear_dvec2(p1, p2); - assert_eq!(linear.curvature(TValue::Parametric(0.)), 0.); - assert_eq!(linear.curvature(TValue::Parametric(0.5)), 0.); - assert_eq!(linear.curvature(TValue::Parametric(1.)), 0.); - - let quadratic = Bezier::from_quadratic_dvec2(p1, p2, p3); - assert!(compare_f64s(quadratic.curvature(TValue::Parametric(0.)), 0.0125)); - assert!(compare_f64s(quadratic.curvature(TValue::Parametric(0.5)), 0.035355)); - assert!(compare_f64s(quadratic.curvature(TValue::Parametric(1.)), 0.0125)); - - let cubic = Bezier::from_cubic_dvec2(p1, p2, p3, p4); - assert!(compare_f64s(cubic.curvature(TValue::Parametric(0.)), 0.016667)); - assert!(compare_f64s(cubic.curvature(TValue::Parametric(0.5)), 0.)); - assert!(compare_f64s(cubic.curvature(TValue::Parametric(1.)), 0.)); - - // The curvature at an inflection point is zero - let inflection_curve = Bezier::from_cubic_coordinates(30., 30., 30., 150., 150., 30., 150., 150.); - let inflections = inflection_curve.inflections(); - assert_eq!(inflection_curve.curvature(TValue::Parametric(inflections[0])), 0.); - } - - #[test] - fn test_extrema_linear() { - // Linear bezier cannot have extrema - let line = Bezier::from_linear_dvec2(DVec2::new(10., 10.), DVec2::new(50., 50.)); - let [x_extrema, y_extrema] = line.local_extrema(); - assert_eq!(y_extrema.count(), 0); - assert_eq!(x_extrema.count(), 0); - } - - #[test] - fn test_extrema_quadratic() { - // Test with no x-extrema, no y-extrema - let bezier1 = Bezier::from_quadratic_coordinates(40., 35., 149., 54., 155., 170.); - let [x_extrema1, y_extrema1] = bezier1.local_extrema(); - assert_eq!(x_extrema1.count(), 0); - assert_eq!(y_extrema1.count(), 0); - - // Test with 1 x-extrema, no y-extrema - let bezier2 = Bezier::from_quadratic_coordinates(45., 30., 170., 90., 45., 150.); - let [x_extrema2, y_extrema2] = bezier2.local_extrema(); - assert_eq!(x_extrema2.count(), 1); - assert_eq!(y_extrema2.count(), 0); - - // Test with no x-extrema, 1 y-extrema - let bezier3 = Bezier::from_quadratic_coordinates(30., 130., 100., 25., 150., 130.); - let [x_extrema3, y_extrema3] = bezier3.local_extrema(); - assert_eq!(x_extrema3.count(), 0); - assert_eq!(y_extrema3.count(), 1); - - // Test with 1 x-extrema, 1 y-extrema - let bezier4 = Bezier::from_quadratic_coordinates(50., 70., 170., 35., 60., 150.); - let [x_extrema4, y_extrema4] = bezier4.local_extrema(); - assert_eq!(x_extrema4.count(), 1); - assert_eq!(y_extrema4.count(), 1); - } - - #[test] - fn test_extrema_cubic() { - // 0 x-extrema, 0 y-extrema - let bezier1 = Bezier::from_cubic_coordinates(100., 105., 250., 250., 110., 150., 260., 260.); - let [x_extrema1, y_extrema1] = bezier1.local_extrema(); - assert_eq!(x_extrema1.count(), 0); - assert_eq!(y_extrema1.count(), 0); - - // 1 x-extrema, 0 y-extrema - let bezier2 = Bezier::from_cubic_coordinates(55., 145., 40., 40., 110., 110., 180., 40.); - let [x_extrema2, y_extrema2] = bezier2.local_extrema(); - assert_eq!(x_extrema2.count(), 1); - assert_eq!(y_extrema2.count(), 0); - - // 1 x-extrema, 1 y-extrema - let bezier3 = Bezier::from_cubic_coordinates(100., 105., 170., 10., 25., 20., 20., 120.); - let [x_extrema3, y_extrema3] = bezier3.local_extrema(); - assert_eq!(x_extrema3.count(), 1); - assert_eq!(y_extrema3.count(), 1); - - // 1 x-extrema, 2 y-extrema - let bezier4 = Bezier::from_cubic_coordinates(50., 90., 120., 16., 150., 190., 45., 150.); - let [x_extrema4, y_extrema4] = bezier4.local_extrema(); - assert_eq!(x_extrema4.count(), 1); - assert_eq!(y_extrema4.count(), 2); - - // 2 x-extrema, 0 y-extrema - let bezier5 = Bezier::from_cubic_coordinates(40., 170., 150., 160., 10., 10., 170., 10.); - let [x_extrema5, y_extrema5] = bezier5.local_extrema(); - assert_eq!(x_extrema5.count(), 2); - assert_eq!(y_extrema5.count(), 0); - - // 2 x-extrema, 1 y-extrema - let bezier6 = Bezier::from_cubic_coordinates(40., 170., 150., 160., 10., 10., 160., 45.); - let [x_extrema6, y_extrema6] = bezier6.local_extrema(); - assert_eq!(x_extrema6.count(), 2); - assert_eq!(y_extrema6.count(), 1); - - // 2 x-extrema, 2 y-extrema - let bezier7 = Bezier::from_cubic_coordinates(46., 60., 140., 10., 50., 160., 120., 120.); - let [x_extrema7, y_extrema7] = bezier7.local_extrema(); - assert_eq!(x_extrema7.count(), 2); - assert_eq!(y_extrema7.count(), 2); - } - - #[test] - fn test_bounding_box() { - // Case where the start and end points dictate the bounding box - let bezier_simple = Bezier::from_linear_coordinates(0., 0., 10., 10.); - assert_eq!(bezier_simple.bounding_box(), [DVec2::new(0., 0.), DVec2::new(10., 10.)]); - - // Case where the curve's extrema dictate the bounding box - let bezier_complex = Bezier::from_cubic_coordinates(90., 70., 25., 25., 175., 175., 110., 130.); - assert!(compare_vec_of_points( - bezier_complex.bounding_box().to_vec(), - vec![DVec2::new(73.2774, 61.4755), DVec2::new(126.7226, 138.5245)], - 1e-3 - )); - } - - #[test] - fn test_find_tvalues_for_x() { - struct Assertion { - bezier: Bezier, - x: f64, - ys: &'static [f64], - } - - let assertions = [ - Assertion { - bezier: Bezier::from_linear_coordinates(0., 0., 20., 10.), - x: 5., - ys: &[2.5], - }, - Assertion { - bezier: Bezier::from_quadratic_coordinates(0., 0., 10., 5., 20., 10.), - x: 5., - ys: &[2.5], - }, - Assertion { - bezier: Bezier::from_cubic_coordinates(0., 0., 10., 5., 10., 5., 20., 10.), - x: 5., - ys: &[2.5], - }, - Assertion { - bezier: Bezier::from_cubic_coordinates(90., 70., 25., 25., 175., 175., 110., 130.), - x: 100., - ys: &[100.], - }, - Assertion { - bezier: Bezier::from_cubic_coordinates(90., 70., 25., 25., 175., 175., 110., 130.), - x: 80., - ys: &[63.62683, 74.53867], - }, - Assertion { - bezier: Bezier::from_cubic_coordinates(110., 70., 25., 25., 175., 175., 90., 130.), - x: 100., - ys: &[65.11345, 100., 134.88655], - }, - ]; - - for Assertion { bezier, x, ys } in assertions { - let mut got: Vec = bezier - .find_tvalues_for_x(x) - .map(|t| bezier.evaluate(TValue::Parametric(t))) - .inspect(|p| assert!((p.x - x).abs() < 1e-4, "wrong x-coordinate, got {} expected {x}", p.x)) - .map(|p| p.y) - .collect(); - assert_eq!(got.len(), ys.len()); - got.sort_by(f64::total_cmp); - got.into_iter() - .zip(ys) - .for_each(|(got, &expected)| assert!((got - expected).abs() < 1e-4, "wrong y-coordinate, got {got} expected {expected}")); - } - } - - #[test] - fn test_inflections() { - let bezier = Bezier::from_cubic_coordinates(30., 30., 30., 150., 150., 30., 150., 150.); - let inflections = bezier.inflections(); - assert_eq!(inflections.len(), 1); - assert_eq!(inflections[0], 0.5); - } - - #[test] - fn test_intersect_line_segment_linear() { - let p1 = DVec2::new(30., 60.); - let p2 = DVec2::new(140., 120.); - - // Intersection at edge of curve - let bezier = Bezier::from_linear_dvec2(p1, p2); - let line1 = Bezier::from_linear_coordinates(20., 60., 70., 60.); - let intersections1 = bezier.intersections(&line1, None, None); - assert!(intersections1.len() == 1); - assert!(compare_points(bezier.evaluate(TValue::Parametric(intersections1[0])), DVec2::new(30., 60.))); - - // Intersection in the middle of curve - let line2 = Bezier::from_linear_coordinates(150., 150., 30., 30.); - let intersections2 = bezier.intersections(&line2, None, None); - assert!(compare_points(bezier.evaluate(TValue::Parametric(intersections2[0])), DVec2::new(96., 96.))); - } - - #[test] - fn test_intersect_line_segment_quadratic() { - let p1 = DVec2::new(30., 50.); - let p2 = DVec2::new(140., 30.); - let p3 = DVec2::new(160., 170.); - - // Intersection at edge of curve - let bezier = Bezier::from_quadratic_dvec2(p1, p2, p3); - let line1 = Bezier::from_linear_coordinates(20., 50., 40., 50.); - let intersections1 = bezier.intersections(&line1, None, None); - assert!(intersections1.len() == 1); - assert!(compare_points(bezier.evaluate(TValue::Parametric(intersections1[0])), p1)); - - // Intersection in the middle of curve - let line2 = Bezier::from_linear_coordinates(150., 150., 30., 30.); - let intersections2 = bezier.intersections(&line2, None, None); - assert!(compare_points(bezier.evaluate(TValue::Parametric(intersections2[0])), DVec2::new(47.77355, 47.77354))); - } - - #[test] - fn test_intersect_line_segment_cubic() { - let p1 = DVec2::new(30., 30.); - let p2 = DVec2::new(60., 140.); - let p3 = DVec2::new(150., 30.); - let p4 = DVec2::new(160., 160.); - - let bezier = Bezier::from_cubic_dvec2(p1, p2, p3, p4); - // Intersection at edge of curve, Discriminant > 0 - let line1 = Bezier::from_linear_coordinates(20., 30., 40., 30.); - let intersections1 = bezier.intersections(&line1, None, None); - assert!(intersections1.len() == 1); - assert!(compare_points(bezier.evaluate(TValue::Parametric(intersections1[0])), p1)); - - // Intersection at edge and in middle of curve, Discriminant < 0 - let line2 = Bezier::from_linear_coordinates(150., 150., 30., 30.); - let intersections2 = bezier.intersections(&line2, None, None); - assert!(intersections2.len() == 2); - assert!(compare_points(bezier.evaluate(TValue::Parametric(intersections2[0])), p1)); - assert!(compare_points(bezier.evaluate(TValue::Parametric(intersections2[1])), DVec2::new(85.84, 85.84))); - } - - #[test] - fn test_intersect_curve_cubic_anchor_handle_overlap() { - // M31 94 C40 40 107 107 106 106 - - let p1 = DVec2::new(31., 94.); - let p2 = DVec2::new(40., 40.); - let p3 = DVec2::new(107., 107.); - let p4 = DVec2::new(106., 106.); - let bezier = Bezier::from_cubic_dvec2(p1, p2, p3, p4); - - let line = Bezier::from_linear_coordinates(150., 150., 20., 20.); - let intersections = bezier.intersections(&line, None, None); - - assert_eq!(intersections.len(), 1); - assert!(compare_points(bezier.evaluate(TValue::Parametric(intersections[0])), p4)); - } - - #[test] - fn test_intersect_curve_cubic_edge_case() { - // M34 107 C40 40 120 120 102 29 - - let p1 = DVec2::new(34., 107.); - let p2 = DVec2::new(40., 40.); - let p3 = DVec2::new(120., 120.); - let p4 = DVec2::new(102., 29.); - let bezier = Bezier::from_cubic_dvec2(p1, p2, p3, p4); - - let line = Bezier::from_linear_coordinates(150., 150., 20., 20.); - let intersections = bezier.intersections(&line, None, None); - - assert_eq!(intersections.len(), 1); - } - - #[test] - fn test_intersect_curve() { - let bezier1 = Bezier::from_cubic_coordinates(30., 30., 60., 140., 150., 30., 160., 160.); - let bezier2 = Bezier::from_quadratic_coordinates(175., 140., 20., 20., 120., 20.); - - let intersections1 = bezier1.intersections(&bezier2, None, None); - let intersections2 = bezier2.intersections(&bezier1, None, None); - - let intersections1_points: Vec = intersections1.iter().map(|&t| bezier1.evaluate(TValue::Parametric(t))).collect(); - let intersections2_points: Vec = intersections2.iter().map(|&t| bezier2.evaluate(TValue::Parametric(t))).rev().collect(); - - assert!(compare_vec_of_points(intersections1_points, intersections2_points, 2.)); - } - - #[test] - fn test_intersect_with_self() { - let bezier = Bezier::from_cubic_coordinates(160., 180., 170., 10., 30., 90., 180., 140.); - let intersections = bezier.self_intersections(Some(0.5), None); - assert!(compare_vec_of_points( - intersections.iter().map(|&t| bezier.evaluate(TValue::Parametric(t[0]))).collect(), - intersections.iter().map(|&t| bezier.evaluate(TValue::Parametric(t[1]))).collect(), - 2. - )); - assert!(Bezier::from_linear_coordinates(160., 180., 170., 10.).self_intersections(None, None).is_empty()); - assert!(Bezier::from_quadratic_coordinates(160., 180., 170., 10., 30., 90.).self_intersections(None, None).is_empty()); - } -} diff --git a/libraries/bezier-rs/src/bezier/structs.rs b/libraries/bezier-rs/src/bezier/structs.rs deleted file mode 100644 index 088d541bef..0000000000 --- a/libraries/bezier-rs/src/bezier/structs.rs +++ /dev/null @@ -1,71 +0,0 @@ -use glam::DVec2; -use std::fmt::{Debug, Formatter, Result}; - -/// Struct used to represent the different strategies for generating arc approximations. -#[derive(Copy, Clone)] -pub enum ArcStrategy { - /// Start with the greedy strategy of maximizing arc approximations and automatically switch to the divide-and-conquer when the greedy approximations no longer fall within the error bound. - Automatic, - /// Use the greedy strategy to maximize approximated arcs, despite potentially erroneous arcs. - FavorLargerArcs, - /// Use the divide-and-conquer strategy that prioritizes correctness over maximal arcs. - FavorCorrectness, -} - -/// Struct to represent optional parameters that can be passed to the `arcs` function. -#[derive(Copy, Clone)] -pub struct ArcsOptions { - /// Determines how the approximated arcs are computed. - /// When maximizing the arcs, the algorithm may return incorrect arcs when the curve contains any small loops or segments that look like a very thin "U". - /// The enum options behave as follows: - /// - `Automatic`: Maximize arcs until an erroneous approximation is found. Compute the arcs of the rest of the curve by first splitting on extremas to ensure no more erroneous cases are encountered. - /// - `FavorLargerArcs`: Maximize arcs using the original algorithm from the [Approximating a Bezier curve with circular arcs](https://pomax.github.io/bezierinfo/#arcapproximation) section of Pomax's bezier curve primer. Erroneous arcs are possible. - /// - `FavorCorrectness`: Prioritize correctness by first spliting the curve by its extremas and determine the arc approximation of each segment instead. - /// - /// The default value is `Automatic`. - pub strategy: ArcStrategy, - /// The error used for approximating the arc's fit. The default is `0.5`. - pub error: f64, - /// The maximum number of segment iterations used as attempts for arc approximations. The default is `100`. - pub max_iterations: usize, -} - -impl Default for ArcsOptions { - fn default() -> Self { - Self { - strategy: ArcStrategy::Automatic, - error: 0.5, - max_iterations: 100, - } - } -} - -/// Struct to represent the circular arc approximation used in the `arcs` bezier function. -#[derive(Copy, Clone, PartialEq)] -pub struct CircleArc { - /// The center point of the circle. - pub center: DVec2, - /// The radius of the circle. - pub radius: f64, - /// The start angle of the circle sector in rad. - pub start_angle: f64, - /// The end angle of the circle sector in rad. - pub end_angle: f64, -} - -impl Debug for CircleArc { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - write!(f, "Center: {}, radius: {}, start to end angles: {} to {}", self.center, self.radius, self.start_angle, self.end_angle) - } -} - -impl Default for CircleArc { - fn default() -> Self { - Self { - center: DVec2::ZERO, - radius: 0., - start_angle: 0., - end_angle: 0., - } - } -} diff --git a/libraries/bezier-rs/src/bezier/transform.rs b/libraries/bezier-rs/src/bezier/transform.rs deleted file mode 100644 index 6a48dc73bb..0000000000 --- a/libraries/bezier-rs/src/bezier/transform.rs +++ /dev/null @@ -1,1053 +0,0 @@ -use super::*; -use crate::compare::compare_points; -use crate::utils::{Cap, TValue, f64_compare}; -use crate::{AppendType, ManipulatorGroup, Subpath}; -use glam::DMat2; -use std::f64::consts::PI; - -/// Functionality that transform Beziers, such as split, reduce, offset, etc. -impl Bezier { - /// Returns a linear approximation of the given [Bezier]. For higher order [Bezier], this means simply dropping the handles. - pub fn to_linear(&self) -> Bezier { - Bezier::from_linear_dvec2(self.start(), self.end()) - } - - /// Returns a quadratic approximation of the given [Bezier]. For cubic Bezier, which typically cannot be represented by a single - /// quadratic segment, this function simply takes the average of the cubic handles to be the new quadratic handle. - pub fn to_quadratic(&self) -> Bezier { - let handle = match self.handles { - BezierHandles::Linear => self.start, - BezierHandles::Quadratic { handle } => handle, - BezierHandles::Cubic { handle_start, handle_end } => (handle_start + handle_end) / 2., - }; - Bezier::from_quadratic_dvec2(self.start, handle, self.end) - } - - /// Returns a cubic approximation of the given [Bezier]. - pub fn to_cubic(&self) -> Bezier { - let (handle_start, handle_end) = match self.handles { - BezierHandles::Linear => (self.start, self.end), - // Conversion reference source: https://stackoverflow.com/a/63059651/775283 - BezierHandles::Quadratic { handle } => (self.start + (2. / 3.) * (handle - self.start), self.end + (2. / 3.) * (handle - self.end)), - BezierHandles::Cubic { handle_start: _, handle_end: _ } => return *self, - }; - Bezier::from_cubic_dvec2(self.start, handle_start, handle_end, self.end) - } - - /// Returns the pair of Bezier curves that result from splitting the original curve at the point `t` along the curve. - /// - pub fn split(&self, t: TValue) -> [Bezier; 2] { - let t = self.t_value_to_parametric(t); - let split_point = self.evaluate(TValue::Parametric(t)); - - match self.handles { - BezierHandles::Linear => [Bezier::from_linear_dvec2(self.start, split_point), Bezier::from_linear_dvec2(split_point, self.end)], - // TODO: Actually calculate the correct handle locations - BezierHandles::Quadratic { handle } => { - let t_minus_one = t - 1.; - [ - Bezier::from_quadratic_dvec2(self.start, t * handle - t_minus_one * self.start, split_point), - Bezier::from_quadratic_dvec2(split_point, t * self.end - t_minus_one * handle, self.end), - ] - } - BezierHandles::Cubic { handle_start, handle_end } => { - let t_minus_one = t - 1.; - [ - Bezier::from_cubic_dvec2( - self.start, - t * handle_start - t_minus_one * self.start, - (t * t) * handle_end - 2. * t * t_minus_one * handle_start + (t_minus_one * t_minus_one) * self.start, - split_point, - ), - Bezier::from_cubic_dvec2( - split_point, - (t * t) * self.end - 2. * t * t_minus_one * handle_end + (t_minus_one * t_minus_one) * handle_start, - t * self.end - t_minus_one * handle_end, - self.end, - ), - ] - } - } - } - - /// Returns a reversed version of the Bezier curve. - pub fn reverse(&self) -> Bezier { - match self.handles { - BezierHandles::Linear => Bezier::from_linear_dvec2(self.end, self.start), - BezierHandles::Quadratic { handle } => Bezier::from_quadratic_dvec2(self.end, handle, self.start), - BezierHandles::Cubic { handle_start, handle_end } => Bezier::from_cubic_dvec2(self.end, handle_end, handle_start, self.start), - } - } - - /// Returns the Bezier curve representing the sub-curve between the two provided points. - /// It will start at the point corresponding to the smaller of `t1` and `t2`, and end at the point corresponding to the larger of `t1` and `t2`. - /// - pub fn trim(&self, t1: TValue, t2: TValue) -> Bezier { - let (mut t1, mut t2) = (self.t_value_to_parametric(t1), self.t_value_to_parametric(t2)); - // If t1 is equal to t2, return a bezier comprised entirely of the same point - if f64_compare(t1, t2, MAX_ABSOLUTE_DIFFERENCE) { - let point = self.evaluate(TValue::Parametric(t1)); - return match self.handles { - BezierHandles::Linear => Bezier::from_linear_dvec2(point, point), - BezierHandles::Quadratic { handle: _ } => Bezier::from_quadratic_dvec2(point, point, point), - BezierHandles::Cubic { handle_start: _, handle_end: _ } => Bezier::from_cubic_dvec2(point, point, point, point), - }; - } else if t1 > t2 { - (t1, t2) = (t2, t1) - } - let bezier_ending_at_t2 = self.split(TValue::Parametric(t2))[0]; - // Adjust the ratio `t1` to its corresponding value on the new curve that was split on `t2` - let adjusted_t1 = t1 / t2; - bezier_ending_at_t2.split(TValue::Parametric(adjusted_t1))[1] - } - - /// Returns a Bezier curve that results from applying the transformation function to each point in the Bezier. - pub fn apply_transformation(&self, transformation_function: impl Fn(DVec2) -> DVec2) -> Bezier { - Self { - start: transformation_function(self.start), - end: transformation_function(self.end), - handles: self.handles.apply_transformation(transformation_function), - } - } - - /// Returns a Bezier curve that results from rotating the curve around the origin by the given angle (in radians). - /// - pub fn rotate(&self, angle: f64) -> Bezier { - let rotation_matrix = DMat2::from_angle(angle); - self.apply_transformation(|point| rotation_matrix.mul_vec2(point)) - } - - /// Returns a Bezier curve that results from rotating the curve around the provided point by the given angle (in radians). - pub fn rotate_about_point(&self, angle: f64, pivot: DVec2) -> Bezier { - let rotation_matrix = DMat2::from_angle(angle); - self.apply_transformation(|point| rotation_matrix.mul_vec2(point - pivot) + pivot) - } - - /// Returns a Bezier curve that results from translating the curve by the given `DVec2`. - pub fn translate(&self, translation: DVec2) -> Bezier { - self.apply_transformation(|point| point + translation) - } - - /// Determine if it is possible to scale the given curve, using the following conditions: - /// 1. All the handles are located on a single side of the curve. - /// 2. The on-curve point for `t = 0.5` must occur roughly in the center of the polygon defined by the curve's endpoint normals. - /// - /// See [the offset section](https://pomax.github.io/bezierinfo/#offsetting) of Pomax's bezier curve primer for more details. - fn is_scalable(&self) -> bool { - if self.handles == BezierHandles::Linear { - return true; - } - // Verify all the handles are located on a single side of the curve. - if let BezierHandles::Cubic { handle_start, handle_end } = self.handles { - let angle_1 = (self.end - self.start).angle_to(handle_start - self.start); - let angle_2 = (self.end - self.start).angle_to(handle_end - self.start); - if (angle_1 > 0. && angle_2 < 0.) || (angle_1 < 0. && angle_2 > 0.) { - return false; - } - } - // Verify the angle formed by the endpoint normals is sufficiently small, ensuring the on-curve point for `t = 0.5` occurs roughly in the center of the polygon. - let normal_0 = self.normal(TValue::Parametric(0.)); - let normal_1 = self.normal(TValue::Parametric(1.)); - let endpoint_normal_angle = (normal_0.x * normal_1.x + normal_0.y * normal_1.y).min(1.).acos(); - endpoint_normal_angle < SCALABLE_CURVE_MAX_ENDPOINT_NORMAL_ANGLE - } - - /// Add the bezier endpoints if not already present, and combine and sort the dimensional extrema. - pub(crate) fn get_extrema_t_list(&self) -> Vec { - let mut extrema = self.local_extrema().into_iter().flatten().collect::>(); - extrema.append(&mut vec![0., 1.]); - extrema.sort_by(|ex1, ex2| ex1.partial_cmp(ex2).unwrap()); - extrema.dedup(); - extrema - } - - /// Returns a tuple of the scalable subcurves and the corresponding `t` values that were used to split the curve. - /// This function may introduce gaps if subsections of the curve are not reducible. - /// The function takes the following parameter: - /// - `step_size` - Dictates the granularity at which the function searches for reducible subcurves. The default value is `0.01`. - /// A small granularity may increase the chance the function does not introduce gaps, but will increase computation time. - pub(crate) fn reduced_curves_and_t_values(&self, step_size: Option) -> (Vec, Vec<[f64; 2]>) { - // A linear segment is scalable, so return itself - if let BezierHandles::Linear = self.handles { - return (vec![*self], vec![[0., 1.]]); - } - - let step_size = step_size.unwrap_or(DEFAULT_REDUCE_STEP_SIZE); - - let mut extrema = self.get_extrema_t_list(); - if let BezierHandles::Cubic { handle_start: _, handle_end: _ } = self.handles { - extrema.append(&mut self.inflections()); - extrema.sort_by(|ex1, ex2| ex1.partial_cmp(ex2).unwrap()); - } - - // Split each subcurve such that each resulting segment is scalable. - let mut result_beziers: Vec = Vec::new(); - let mut result_t_values: Vec<[f64; 2]> = vec![]; - - extrema.windows(2).for_each(|t_pair| { - let t_subcurve_start = t_pair[0]; - let t_subcurve_end = t_pair[1]; - let subcurve = self.trim(TValue::Parametric(t_subcurve_start), TValue::Parametric(t_subcurve_end)); - // Perform no processing on the subcurve if it's already scalable. - if subcurve.is_scalable() { - result_beziers.push(subcurve); - result_t_values.push([t_subcurve_start, t_subcurve_end]); - return; - } - - // Greedily iterate across the subcurve at intervals of size `step_size` to break up the curve into maximally large segments - let mut segment: Bezier; - let mut t1 = 0.; - let mut t2 = step_size; - let mut is_prev_valid = false; - while t2 <= 1. + step_size { - segment = subcurve.trim(TValue::Parametric(t1), TValue::Parametric(f64::min(t2, 1.))); - if !segment.is_scalable() { - t2 -= step_size; - - // If the previous step does not exist, the start of the subcurve is irreducible. - // Otherwise, add the valid segment from the previous step to the result. - if is_prev_valid { - segment = subcurve.trim(TValue::Parametric(t1), TValue::Parametric(t2)); - if segment.is_scalable() { - result_beziers.push(segment); - result_t_values.push([t_subcurve_start + t1 * (t_subcurve_end - t_subcurve_start), t_subcurve_start + t2 * (t_subcurve_end - t_subcurve_start)]); - } else { - t2 = t1 + step_size; - } - } else { - t2 = t1 + step_size; - } - t1 = t2; - is_prev_valid = false; - } else { - is_prev_valid = true; - } - t2 += step_size; - } - // Collect final remainder of the curve. - if t1 < 1. { - segment = subcurve.trim(TValue::Parametric(t1), TValue::Parametric(1.)); - if segment.is_scalable() { - result_beziers.push(segment); - result_t_values.push([t_subcurve_start + t1 * (t_subcurve_end - t_subcurve_start), t_subcurve_end]); - } - } - }); - (result_beziers, result_t_values) - } - - /// Split the curve into a number of scalable subcurves. This function may introduce gaps if subsections of the curve are not reducible. - /// The function takes the following parameter: - /// - `step_size` - Dictates the granularity at which the function searches for reducible subcurves. The default value is `0.01`. - /// A small granularity may increase the chance the function does not introduce gaps, but will increase computation time. - /// - pub fn reduce(&self, step_size: Option) -> Vec { - self.reduced_curves_and_t_values(step_size).0 - } - - /// Scale will translate a bezier curve a fixed distance away from its original position, and stretch/compress the transformed curve to match the translation ratio. - /// Note that not all bezier curves are possible to scale, so this function asserts that the provided curve is scalable. - /// A proof for why this is true can be found in the [Curve offsetting section](https://pomax.github.io/bezierinfo/#offsetting) of Pomax's bezier curve primer. - /// `scale` takes the parameter `distance`, which is the distance away from the curve that the new one will be scaled to. Positive values will scale the curve in the - /// same direction as the endpoint normals, while negative values will scale in the opposite direction. - fn scale(&self, distance: f64) -> Bezier { - assert!(self.is_scalable(), "The curve provided to scale is not scalable. Reduce the curve first."); - - let normal_start = self.normal(TValue::Parametric(0.)); - let normal_end = self.normal(TValue::Parametric(1.)); - - // If normal unit vectors are equal, then the lines are parallel - if normal_start.abs_diff_eq(normal_end, MAX_ABSOLUTE_DIFFERENCE) { - return self.translate(distance * normal_start); - } - - // Find the intersection point of the endpoint normals - let intersection = utils::line_intersection(self.start, normal_start, self.end, normal_end); - - // If the Bezier is a quadratic, convert it to a cubic to increase expressiveness - let intermediate = match self.handles { - BezierHandles::Quadratic { handle: _ } => self.to_cubic(), - _ => *self, - }; - - let should_flip_direction = (self.start - intersection).normalize().abs_diff_eq(normal_start, MAX_ABSOLUTE_DIFFERENCE); - intermediate.apply_transformation(|point| { - let mut direction_unit_vector = (intersection - point).normalize(); - if should_flip_direction { - direction_unit_vector *= -1.; - } - point + distance * direction_unit_vector - }) - } - - /// Version of the `scale` function which scales the curve such that the start of the scaled curve is `start_distance` from the original curve, while the end of - /// of the scaled curve is `end_distance` from the original curve. The curve transitions from `start_distance` to `end_distance` gradually, proportional to the - /// distance along the equation (`t`-value) of the curve. - pub fn graduated_scale(&self, start_distance: f64, end_distance: f64) -> Bezier { - assert!(self.is_scalable(), "The curve provided to scale is not scalable. Reduce the curve first."); - - // If the Bezier is a quadratic, convert it to a cubic to increase expressiveness - let intermediate = match self.handles { - BezierHandles::Quadratic { handle: _ } => self.to_cubic(), - _ => *self, - }; - - let normal_start = intermediate.normal(TValue::Parametric(0.)); - let normal_end = intermediate.normal(TValue::Parametric(1.)); - - // If normal unit vectors are equal, then the lines are parallel - if normal_start.abs_diff_eq(normal_end, MAX_ABSOLUTE_DIFFERENCE) { - let transformed_start = utils::scale_point_from_direction_vector(intermediate.start, intermediate.normal(TValue::Parametric(0.)), false, start_distance); - let transformed_end = utils::scale_point_from_direction_vector(intermediate.end, intermediate.normal(TValue::Parametric(1.)), false, end_distance); - - return match intermediate.handles { - BezierHandles::Linear => Bezier::from_linear_dvec2(transformed_start, transformed_end), - BezierHandles::Quadratic { handle: _ } => unreachable!(), - BezierHandles::Cubic { handle_start, handle_end } => { - let handle_start_closest_t = intermediate.project(handle_start); - let handle_start_scale_distance = (1. - handle_start_closest_t) * start_distance + handle_start_closest_t * end_distance; - let transformed_handle_start = - utils::scale_point_from_direction_vector(handle_start, intermediate.normal(TValue::Parametric(handle_start_closest_t)), false, handle_start_scale_distance); - - let handle_end_closest_t = intermediate.project(handle_start); - let handle_end_scale_distance = (1. - handle_end_closest_t) * start_distance + handle_end_closest_t * end_distance; - let transformed_handle_end = utils::scale_point_from_direction_vector(handle_end, intermediate.normal(TValue::Parametric(handle_end_closest_t)), false, handle_end_scale_distance); - Bezier::from_cubic_dvec2(transformed_start, transformed_handle_start, transformed_handle_end, transformed_end) - } - }; - } - - // Find the intersection point of the endpoint normals - let intersection = utils::line_intersection(intermediate.start, normal_start, intermediate.end, normal_end); - let should_flip_direction = (intermediate.start - intersection).normalize().abs_diff_eq(normal_start, MAX_ABSOLUTE_DIFFERENCE); - - let transformed_start = utils::scale_point_from_origin(intermediate.start, intersection, should_flip_direction, start_distance); - let transformed_end = utils::scale_point_from_origin(intermediate.end, intersection, should_flip_direction, end_distance); - - match intermediate.handles { - BezierHandles::Linear => Bezier::from_linear_dvec2(transformed_start, transformed_end), - BezierHandles::Quadratic { handle: _ } => unreachable!(), - BezierHandles::Cubic { handle_start, handle_end } => { - let handle_start_scale_distance = (start_distance * 2. + end_distance) / 3.; - let transformed_handle_start = utils::scale_point_from_origin(handle_start, intersection, should_flip_direction, handle_start_scale_distance); - - let handle_end_scale_distance = (start_distance + end_distance * 2.) / 3.; - let transformed_handle_end = utils::scale_point_from_origin(handle_end, intersection, should_flip_direction, handle_end_scale_distance); - Bezier::from_cubic_dvec2(transformed_start, transformed_handle_start, transformed_handle_end, transformed_end) - } - } - } - - /// Offset will break down the Bezier into reducible subcurves, and scale each subcurve a set distance from the original curve. - /// Note that not all bezier curves are possible to offset, so this function first reduces the curve to scalable segments and then offsets those segments. - /// A proof for why this is true can be found in the [Curve offsetting section](https://pomax.github.io/bezierinfo/#offsetting) of Pomax's bezier curve primer. - /// Offset takes the following parameter: - /// - `distance` - The offset's distance from the curve. Positive values will offset the curve in the same direction as the endpoint normals, - /// while negative values will offset in the opposite direction. - /// - pub fn offset(&self, distance: f64) -> Subpath { - if self.is_point() { - return Subpath::from_bezier(self); - } - let reduced = self.reduce(None); - let mut scaled = Subpath::new(vec![], false); - reduced.iter().enumerate().for_each(|(index, bezier)| { - let scaled_bezier = bezier.scale(distance); - if !bezier.is_point() { - if index > 0 && !compare_points(bezier.start(), reduced[index - 1].end()) { - scaled.append_bezier(&scaled_bezier, AppendType::SmoothJoin(MAX_ABSOLUTE_DIFFERENCE)); - } else { - scaled.append_bezier(&scaled_bezier, AppendType::IgnoreStart); - } - } - }); - - // If the curve is not linear, smooth the handles. All segments produced by bezier::scale will be cubic. - if self.handles != BezierHandles::Linear { - scaled.smooth_open_subpath(); - } - - scaled - } - - /// Version of the `offset` function which scales the offset such that the start of the offset is `start_distance` from the original curve, while the end of - /// of the offset is `end_distance` from the original curve. The curve transitions from `start_distance` to `end_distance` gradually, proportional to the - /// distance along the equation (`t`-value) of the curve. Similarly to the `offset` function, the returned result is an approximation. - pub fn graduated_offset(&self, start_distance: f64, end_distance: f64) -> Subpath { - let reduced = self.reduce(None); - let mut next_start_distance = start_distance; - let distance_difference = end_distance - start_distance; - let total_length = self.length(None); - if total_length < MAX_ABSOLUTE_DIFFERENCE { - return Subpath::new(vec![], false); - } - - let mut result = Subpath::new(vec![], false); - reduced.iter().enumerate().for_each(|(index, bezier)| { - if !bezier.is_point() { - let current_length = bezier.length(None); - let next_end_distance = next_start_distance + (current_length / total_length) * distance_difference; - let scaled_bezier = bezier.graduated_scale(next_start_distance, next_end_distance); - - if index > 0 && !compare_points(bezier.start(), reduced[index - 1].end()) { - result.append_bezier(&scaled_bezier, AppendType::SmoothJoin(MAX_ABSOLUTE_DIFFERENCE)); - } else { - result.append_bezier(&scaled_bezier, AppendType::IgnoreStart); - } - next_start_distance = next_end_distance; - } - }); - - // If the curve is not linear, smooth the handles. All segments produced by bezier::scale will be cubic. - if self.handles != BezierHandles::Linear { - result.smooth_open_subpath(); - } - - result - } - - /// Outline will return a vector of Beziers that creates an outline around the curve at the designated distance away from the curve. - /// It makes use of the `offset` function, thus restrictions applicable to `offset` are relevant to this function as well. - /// The 'caps', the linear segments at opposite ends of the outline, intersect the original curve at the midpoint of the cap. - /// Outline takes the following parameter: - /// - `distance` - The outline's distance from the curve. - /// - pub fn outline(&self, distance: f64, cap: Cap) -> Subpath { - let (pos_offset, neg_offset) = if self.is_point() { - ( - Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::NEG_Y * distance)], false), - Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::Y * distance)], false), - ) - } else { - (self.offset(distance), self.reverse().offset(distance)) - }; - - if pos_offset.is_empty() || neg_offset.is_empty() { - return Subpath::new(vec![], false); - } - - pos_offset.combine_outline(&neg_offset, cap) - } - - /// Version of the `outline` function which draws the outline at the specified distances away from the curve. - /// The outline begins `start_distance` away, and gradually move to being `end_distance` away. - /// - pub fn graduated_outline(&self, start_distance: f64, end_distance: f64, cap: Cap) -> Subpath { - self.skewed_outline(start_distance, end_distance, end_distance, start_distance, cap) - } - - /// Version of the `graduated_outline` function that allows for the 4 corners of the outline to be different distances away from the curve. - /// - pub fn skewed_outline(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64, cap: Cap) -> Subpath { - let (pos_offset, neg_offset) = if self.is_point() { - ( - Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::NEG_Y * distance1)], false), - Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::Y * distance1)], false), - ) - } else { - (self.graduated_offset(distance1, distance2), self.reverse().graduated_offset(distance3, distance4)) - }; - - if pos_offset.is_empty() || neg_offset.is_empty() { - return Subpath::new(vec![], false); - } - - pos_offset.combine_outline(&neg_offset, cap) - } - - /// Approximate a bezier curve with circular arcs. - /// The algorithm can be customized using the [ArcsOptions] structure. - /// - pub fn arcs(&self, arcs_options: ArcsOptions) -> Vec { - let ArcsOptions { - strategy: maximize_arcs, - error, - max_iterations, - } = arcs_options; - - match maximize_arcs { - ArcStrategy::Automatic => { - let (auto_arcs, final_low_t) = self.approximate_curve_with_arcs(0., 1., error, max_iterations, true); - let arc_approximations = self.split(TValue::Parametric(final_low_t))[1].arcs(ArcsOptions { - strategy: ArcStrategy::FavorCorrectness, - error, - max_iterations, - }); - if final_low_t != 1. { [auto_arcs, arc_approximations].concat() } else { auto_arcs } - } - ArcStrategy::FavorLargerArcs => self.approximate_curve_with_arcs(0., 1., error, max_iterations, false).0, - ArcStrategy::FavorCorrectness => self - .get_extrema_t_list() - .windows(2) - .flat_map(|t_pair| self.approximate_curve_with_arcs(t_pair[0], t_pair[1], error, max_iterations, false).0) - .collect::>(), - } - } - - /// Implements an algorithm that approximates a bezier curve with circular arcs. - /// This algorithm uses a method akin to binary search to find an arc that approximates a maximal segment of the curve. - /// Once a maximal arc has been found for a sub-segment of the curve, the algorithm continues by starting again at the end of the previous approximation. - /// More details can be found in the [Approximating a Bezier curve with circular arcs](https://pomax.github.io/bezierinfo/#arcapproximation) section of Pomax's bezier curve primer. - /// A caveat with this algorithm is that it is possible to find erroneous approximations in cases such as in a very narrow `U`. - /// - `stop_when_invalid`: Used to determine whether the algorithm should terminate early if erroneous approximations are encountered. - /// - /// Returns a tuple where the first element is the list of circular arcs and the second is the `t` value where the next segment should start from. - /// The second value will be `1.` except for when `stop_when_invalid` is true and an invalid approximation is encountered. - fn approximate_curve_with_arcs(&self, local_low: f64, local_high: f64, error: f64, max_iterations: usize, stop_when_invalid: bool) -> (Vec, f64) { - let mut low = local_low; - let mut middle = (local_low + local_high) / 2.; - let mut high = local_high; - let mut previous_high = local_high; - - let mut iterations = 0; - let mut previous_arc = CircleArc::default(); - let mut was_previous_good = false; - let mut arcs = Vec::new(); - - // Outer loop to iterate over the curve - while low < local_high { - // Inner loop to find the next maximal segment of the curve that can be approximated with a circular arc - while iterations <= max_iterations { - iterations += 1; - let p1 = self.evaluate(TValue::Parametric(low)); - let p2 = self.evaluate(TValue::Parametric(middle)); - let p3 = self.evaluate(TValue::Parametric(high)); - - let wrapped_center = utils::compute_circle_center_from_points(p1, p2, p3); - // If the segment is linear, move on to next segment - if wrapped_center.is_none() { - previous_high = high; - low = high; - high = 1.; - middle = (low + high) / 2.; - was_previous_good = false; - break; - } - - let center = wrapped_center.unwrap(); - let radius = center.distance(p1); - - let angle_p1 = DVec2::new(1., 0.).angle_to(p1 - center); - let angle_p2 = DVec2::new(1., 0.).angle_to(p2 - center); - let angle_p3 = DVec2::new(1., 0.).angle_to(p3 - center); - - let mut start_angle = angle_p1; - let mut end_angle = angle_p3; - - // Adjust start and end angles of the arc to ensure that it travels in the counter-clockwise direction - if angle_p1 < angle_p3 { - if angle_p2 < angle_p1 || angle_p3 < angle_p2 { - std::mem::swap(&mut start_angle, &mut end_angle); - } - } else if angle_p2 < angle_p1 && angle_p3 < angle_p2 { - std::mem::swap(&mut start_angle, &mut end_angle); - } - - let new_arc = CircleArc { - center, - radius, - start_angle, - end_angle, - }; - - // Use points in between low, middle, and high to evaluate how well the arc approximates the curve - let e1 = self.evaluate(TValue::Parametric((low + middle) / 2.)); - let e2 = self.evaluate(TValue::Parametric((middle + high) / 2.)); - - // Iterate until we find the largest good approximation such that the next iteration is not a good approximation with an arc - if utils::f64_compare(radius, e1.distance(center), error) && utils::f64_compare(radius, e2.distance(center), error) { - // Check if the good approximation is actually valid: the sector angle cannot be larger than 180 degrees (PI radians) - let mut sector_angle = end_angle - start_angle; - if sector_angle < 0. { - sector_angle += 2. * PI; - } - if stop_when_invalid && sector_angle > PI { - return (arcs, low); - } - if high == local_high { - // Found the final arc approximation - arcs.push(new_arc); - low = high; - break; - } - // If the approximation is good, expand the segment by half to try finding a larger good approximation - previous_high = high; - high = (high + (high - low) / 2.).min(local_high); - middle = (low + high) / 2.; - previous_arc = new_arc; - was_previous_good = true; - } else if was_previous_good { - // If the previous approximation was good and the current one is bad, then we use the previous good approximation - arcs.push(previous_arc); - - // Continue searching for approximations for the rest of the curve - low = previous_high; - high = local_high; - middle = low + (high - low) / 2.; - was_previous_good = false; - break; - } else { - // If no good approximation has been seen yet, try again with half the segment - previous_high = high; - high = middle; - middle = low + (high - low) / 2.; - previous_arc = new_arc; - } - } - } - - (arcs, low) - } - - /// Reverses the direction of the bézier. - #[must_use] - pub fn reversed(self) -> Self { - Self { - start: self.end, - end: self.start, - handles: self.handles.reversed(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::EmptyId; - use crate::compare::{compare_arcs, compare_points}; - use crate::utils::{Cap, TValue}; - - #[test] - fn test_split() { - let line = Bezier::from_linear_coordinates(25., 25., 75., 75.); - let [part1, part2] = line.split(TValue::Parametric(0.5)); - - assert_eq!(part1.start(), line.start()); - assert_eq!(part1.end(), line.evaluate(TValue::Parametric(0.5))); - assert_eq!(part1.evaluate(TValue::Parametric(0.5)), line.evaluate(TValue::Parametric(0.25))); - - assert_eq!(part2.start(), line.evaluate(TValue::Parametric(0.5))); - assert_eq!(part2.end(), line.end()); - assert_eq!(part2.evaluate(TValue::Parametric(0.5)), line.evaluate(TValue::Parametric(0.75))); - - let quad_bezier = Bezier::from_quadratic_coordinates(10., 10., 50., 50., 90., 10.); - let [part3, part4] = quad_bezier.split(TValue::Parametric(0.5)); - - assert_eq!(part3.start(), quad_bezier.start()); - assert_eq!(part3.end(), quad_bezier.evaluate(TValue::Parametric(0.5))); - assert_eq!(part3.evaluate(TValue::Parametric(0.5)), quad_bezier.evaluate(TValue::Parametric(0.25))); - - assert_eq!(part4.start(), quad_bezier.evaluate(TValue::Parametric(0.5))); - assert_eq!(part4.end(), quad_bezier.end()); - assert_eq!(part4.evaluate(TValue::Parametric(0.5)), quad_bezier.evaluate(TValue::Parametric(0.75))); - - let cubic_bezier = Bezier::from_cubic_coordinates(10., 10., 50., 50., 90., 10., 40., 50.); - let [part5, part6] = cubic_bezier.split(TValue::Parametric(0.5)); - - assert_eq!(part5.start(), cubic_bezier.start()); - assert_eq!(part5.end(), cubic_bezier.evaluate(TValue::Parametric(0.5))); - assert_eq!(part5.evaluate(TValue::Parametric(0.5)), cubic_bezier.evaluate(TValue::Parametric(0.25))); - - assert_eq!(part6.start(), cubic_bezier.evaluate(TValue::Parametric(0.5))); - assert_eq!(part6.end(), cubic_bezier.end()); - assert_eq!(part6.evaluate(TValue::Parametric(0.5)), cubic_bezier.evaluate(TValue::Parametric(0.75))); - } - - #[test] - fn test_split_at_anchors() { - let start = DVec2::new(30., 50.); - let end = DVec2::new(160., 170.); - - let bezier_quadratic = Bezier::from_quadratic_dvec2(start, DVec2::new(140., 30.), end); - - // Test splitting a quadratic bezier at the startpoint - let [point_bezier1, remainder1] = bezier_quadratic.split(TValue::Parametric(0.)); - assert_eq!(point_bezier1, Bezier::from_quadratic_dvec2(start, start, start)); - assert!(remainder1.abs_diff_eq(&bezier_quadratic, MAX_ABSOLUTE_DIFFERENCE)); - - // Test splitting a quadratic bezier at the endpoint - let [remainder2, point_bezier2] = bezier_quadratic.split(TValue::Parametric(1.)); - assert_eq!(point_bezier2, Bezier::from_quadratic_dvec2(end, end, end)); - assert!(remainder2.abs_diff_eq(&bezier_quadratic, MAX_ABSOLUTE_DIFFERENCE)); - - let bezier_cubic = Bezier::from_cubic_dvec2(start, DVec2::new(60., 140.), DVec2::new(150., 30.), end); - - // Test splitting a cubic bezier at the startpoint - let [point_bezier3, remainder3] = bezier_cubic.split(TValue::Parametric(0.)); - assert_eq!(point_bezier3, Bezier::from_cubic_dvec2(start, start, start, start)); - assert!(remainder3.abs_diff_eq(&bezier_cubic, MAX_ABSOLUTE_DIFFERENCE)); - - // Test splitting a cubic bezier at the endpoint - let [remainder4, point_bezier4] = bezier_cubic.split(TValue::Parametric(1.)); - assert_eq!(point_bezier4, Bezier::from_cubic_dvec2(end, end, end, end)); - assert!(remainder4.abs_diff_eq(&bezier_cubic, MAX_ABSOLUTE_DIFFERENCE)); - } - - #[test] - fn test_trim() { - let line = Bezier::from_linear_coordinates(80., 80., 40., 40.); - let trimmed1 = line.trim(TValue::Parametric(0.25), TValue::Parametric(0.75)); - - assert_eq!(trimmed1.start(), line.evaluate(TValue::Parametric(0.25))); - assert_eq!(trimmed1.end(), line.evaluate(TValue::Parametric(0.75))); - assert_eq!(trimmed1.evaluate(TValue::Parametric(0.5)), line.evaluate(TValue::Parametric(0.5))); - - let quadratic_bezier = Bezier::from_quadratic_coordinates(80., 80., 40., 40., 70., 70.); - let trimmed2 = quadratic_bezier.trim(TValue::Parametric(0.25), TValue::Parametric(0.75)); - - assert_eq!(trimmed2.start(), quadratic_bezier.evaluate(TValue::Parametric(0.25))); - assert_eq!(trimmed2.end(), quadratic_bezier.evaluate(TValue::Parametric(0.75))); - assert_eq!(trimmed2.evaluate(TValue::Parametric(0.5)), quadratic_bezier.evaluate(TValue::Parametric(0.5))); - - let cubic_bezier = Bezier::from_cubic_coordinates(80., 80., 40., 40., 70., 70., 150., 150.); - let trimmed3 = cubic_bezier.trim(TValue::Parametric(0.25), TValue::Parametric(0.75)); - - assert!(trimmed3.start().abs_diff_eq(cubic_bezier.evaluate(TValue::Parametric(0.25)), MAX_ABSOLUTE_DIFFERENCE)); - assert_eq!(trimmed3.end(), cubic_bezier.evaluate(TValue::Parametric(0.75))); - assert_eq!(trimmed3.evaluate(TValue::Parametric(0.5)), cubic_bezier.evaluate(TValue::Parametric(0.5))); - } - - #[test] - fn test_trim_t2_greater_than_t1() { - // Test trimming quadratic curve when t2 > t1 - let bezier_quadratic = Bezier::from_quadratic_coordinates(30., 50., 140., 30., 160., 170.); - let trim1 = bezier_quadratic.trim(TValue::Parametric(0.25), TValue::Parametric(0.75)); - let trim2 = bezier_quadratic.trim(TValue::Parametric(0.75), TValue::Parametric(0.25)); - assert!(trim1.abs_diff_eq(&trim2, MAX_ABSOLUTE_DIFFERENCE)); - - // Test trimming cubic curve when t2 > t1 - let bezier_cubic = Bezier::from_cubic_coordinates(30., 30., 60., 140., 150., 30., 160., 160.); - let trim3 = bezier_cubic.trim(TValue::Parametric(0.25), TValue::Parametric(0.75)); - let trim4 = bezier_cubic.trim(TValue::Parametric(0.75), TValue::Parametric(0.25)); - assert!(trim3.abs_diff_eq(&trim4, MAX_ABSOLUTE_DIFFERENCE)); - } - - #[test] - fn test_rotate() { - let bezier_linear = Bezier::from_linear_coordinates(30., 60., 140., 120.); - let rotated_bezier_linear = bezier_linear.rotate(-PI / 2.); - let expected_bezier_linear = Bezier::from_linear_coordinates(60., -30., 120., -140.); - assert!(rotated_bezier_linear.abs_diff_eq(&expected_bezier_linear, MAX_ABSOLUTE_DIFFERENCE)); - - let bezier_quadratic = Bezier::from_quadratic_coordinates(30., 50., 140., 30., 160., 170.); - let rotated_bezier_quadratic = bezier_quadratic.rotate(PI); - let expected_bezier_quadratic = Bezier::from_quadratic_coordinates(-30., -50., -140., -30., -160., -170.); - assert!(rotated_bezier_quadratic.abs_diff_eq(&expected_bezier_quadratic, MAX_ABSOLUTE_DIFFERENCE)); - - let bezier = Bezier::from_cubic_coordinates(30., 30., 60., 140., 150., 30., 160., 160.); - let rotated_bezier = bezier.rotate(PI / 2.); - let expected_bezier = Bezier::from_cubic_coordinates(-30., 30., -140., 60., -30., 150., -160., 160.); - assert!(rotated_bezier.abs_diff_eq(&expected_bezier, MAX_ABSOLUTE_DIFFERENCE)); - } - - #[test] - fn test_translate() { - let bezier_linear = Bezier::from_linear_coordinates(30., 60., 140., 120.); - let rotated_bezier_linear = bezier_linear.translate(DVec2::new(10., 10.)); - let expected_bezier_linear = Bezier::from_linear_coordinates(40., 70., 150., 130.); - assert!(rotated_bezier_linear.abs_diff_eq(&expected_bezier_linear, MAX_ABSOLUTE_DIFFERENCE)); - - let bezier_quadratic = Bezier::from_quadratic_coordinates(30., 50., 140., 30., 160., 170.); - let rotated_bezier_quadratic = bezier_quadratic.translate(DVec2::new(-10., 10.)); - let expected_bezier_quadratic = Bezier::from_quadratic_coordinates(20., 60., 130., 40., 150., 180.); - assert!(rotated_bezier_quadratic.abs_diff_eq(&expected_bezier_quadratic, MAX_ABSOLUTE_DIFFERENCE)); - - let bezier = Bezier::from_cubic_coordinates(30., 30., 60., 140., 150., 30., 160., 160.); - let translated_bezier = bezier.translate(DVec2::new(10., -10.)); - let expected_bezier = Bezier::from_cubic_coordinates(40., 20., 70., 130., 160., 20., 170., 150.); - assert!(translated_bezier.abs_diff_eq(&expected_bezier, MAX_ABSOLUTE_DIFFERENCE)); - } - - #[test] - fn test_reduce() { - let p1 = DVec2::new(0., 0.); - let p2 = DVec2::new(50., 50.); - let p3 = DVec2::new(0., 0.); - let bezier = Bezier::from_quadratic_dvec2(p1, p2, p3); - - let reduced_curves = bezier.reduce(None); - assert!(reduced_curves.iter().all(|bezier| bezier.is_scalable())); - - // Check that the reduce helper is correct - let (helper_curves, helper_t_values) = bezier.reduced_curves_and_t_values(None); - assert!( - reduced_curves - .iter() - .zip(helper_curves.iter()) - .all(|(bezier1, bezier2)| bezier1.abs_diff_eq(bezier2, MAX_ABSOLUTE_DIFFERENCE)) - ); - assert!( - reduced_curves - .iter() - .zip(helper_t_values.iter()) - .all(|(curve, t_pair)| curve.abs_diff_eq(&bezier.trim(TValue::Parametric(t_pair[0]), TValue::Parametric(t_pair[1])), MAX_ABSOLUTE_DIFFERENCE)) - ) - } - - fn assert_valid_offset(bezier: &Bezier, offset: &Subpath, expected_distance: f64) { - // Verify that the offset is smooth - if offset.len() > 1 { - offset.iter().take(offset.len() - 2).zip(offset.iter().skip(1)).for_each(|beziers_pair| { - assert!(compare_points(beziers_pair.0.end, beziers_pair.1.start)); - assert!(compare_points(beziers_pair.0.normal(TValue::Parametric(1.)), beziers_pair.1.normal(TValue::Parametric(0.)))); - }); - } - - // Verify that the offset spans the length of the curve - let start_distance = bezier.evaluate(TValue::Parametric(0.)).distance(offset.iter().next().unwrap().evaluate(TValue::Parametric(0.))); - assert!(f64_compare(start_distance, expected_distance, MAX_ABSOLUTE_DIFFERENCE)); - let end_distance = bezier.evaluate(TValue::Parametric(1.)).distance(offset.iter().last().unwrap().evaluate(TValue::Parametric(1.))); - assert!(f64_compare(end_distance, expected_distance, MAX_ABSOLUTE_DIFFERENCE)); - - let err_threshold = expected_distance / 10.; - // Sample the curve and verify that the offset lies at the correct distance from the curve. - // Collect the t-value associated with the point on the bezier closest to the sample. - let t_values: Vec = offset - .iter() - .flat_map(|offset_segment| { - [0.1, 0.25, 0.5, 0.75, 0.9] - .iter() - .map(|t| { - let offset_point = offset_segment.evaluate(TValue::Parametric(*t)); - let closest_point_t = bezier.project(offset_point); - let closest_point = bezier.evaluate(TValue::Parametric(closest_point_t)); - let actual_distance = offset_point.distance(closest_point); - - assert!(f64_compare(actual_distance, expected_distance, err_threshold)); - closest_point_t - }) - .collect::>() - }) - .collect(); - - // Verify that the curve segments are in the correct order by asserting that t_values is sorted - for i in 1..t_values.len() { - assert!(t_values[i - 1] < t_values[i]); - } - } - - #[test] - fn test_offset_linear() { - let start = DVec2::new(30., 60.); - let end = DVec2::new(140., 120.); - let bezier = Bezier::from_linear_dvec2(start, end); - - for distance in [-20., -10., 10., 20.] { - let offset = bezier.offset::(distance); - assert_valid_offset(&bezier, &offset, distance.abs()); - } - } - - #[test] - fn test_offset_quadratic() { - let start = DVec2::new(30., 50.); - let handle = DVec2::new(140., 30.); - let end = DVec2::new(160., 170.); - let bezier = Bezier::from_quadratic_dvec2(start, handle, end); - - for distance in [-20., -10., 10., 20.] { - let offset = bezier.offset::(distance); - assert_valid_offset(&bezier, &offset, distance.abs()); - } - } - - #[test] - fn test_offset_cubic() { - let start = DVec2::new(30., 30.); - let handle1 = DVec2::new(60., 140.); - let handle2 = DVec2::new(150., 30.); - let end = DVec2::new(160., 160.); - let bezier = Bezier::from_cubic_dvec2(start, handle1, handle2, end); - - for distance in [-20., -10., 10., 20.] { - let offset = bezier.offset::(distance); - assert_valid_offset(&bezier, &offset, distance.abs()); - } - } - - #[test] - fn test_offset_curve_that_has_a_single_point_after_reduce() { - let p1 = DVec2::new(30., 30.); - let p2 = DVec2::new(150., 29.); - let p3 = DVec2::new(150., 30.); - let p4 = DVec2::new(160., 160.); - - let bezier = Bezier::from_cubic_dvec2(p1, p2, p3, p4); - - let reduce = bezier.reduce(None); - let offset = bezier.offset::(15.); - assert!(reduce.last().is_some()); - assert!(reduce.last().unwrap().is_point()); - // Expect the single point bezier to be dropped in the offset - assert_eq!(reduce.len(), offset.len_segments() + 1); - } - - #[test] - fn test_outline() { - let p1 = DVec2::new(30., 50.); - let p2 = DVec2::new(140., 30.); - let line = Bezier::from_linear_dvec2(p1, p2); - let outline = line.outline::(10., Cap::Butt); - - assert_eq!(outline.len(), 4); - - // Assert the first length-wise piece of the outline is 10 units from the line - assert!(f64_compare( - outline.iter().next().unwrap().evaluate(TValue::Parametric(0.25)).distance(line.evaluate(TValue::Parametric(0.25))), - 10., - MAX_ABSOLUTE_DIFFERENCE - )); // f64 - - // Assert the first cap touches the line end point at the halfway point - assert!(outline.iter().nth(1).unwrap().evaluate(TValue::Parametric(0.5)).abs_diff_eq(line.end(), MAX_ABSOLUTE_DIFFERENCE)); - - // Assert the second length-wise piece of the outline is 10 units from the line - assert!(f64_compare( - outline.iter().nth(2).unwrap().evaluate(TValue::Parametric(0.25)).distance(line.evaluate(TValue::Parametric(0.75))), - 10., - MAX_ABSOLUTE_DIFFERENCE - )); // f64 - - // Assert the second cap touches the line start point at the halfway point - assert!(outline.iter().nth(3).unwrap().evaluate(TValue::Parametric(0.5)).abs_diff_eq(line.start(), MAX_ABSOLUTE_DIFFERENCE)); - } - - #[test] - fn test_outline_single_point_circle() { - let ellipse: Subpath = Subpath::new_ellipse(DVec2::new(0., 0.), DVec2::new(50., 50.)).reverse(); - let p = DVec2::new(25., 25.); - - let line = Bezier::from_linear_dvec2(p, p); - let outline = line.outline::(25., Cap::Round); - assert_eq!(outline, ellipse); - - let cubic = Bezier::from_cubic_dvec2(p, p, p, p); - let outline_cubic = cubic.outline::(25., Cap::Round); - assert_eq!(outline_cubic, ellipse); - } - - #[test] - fn test_outline_single_point_square() { - let square: Subpath = Subpath::from_anchors( - [ - DVec2::new(25., 0.), - DVec2::new(0., 0.), - DVec2::new(0., 50.), - DVec2::new(25., 50.), - DVec2::new(50., 50.), - DVec2::new(50., 0.), - ], - true, - ); - let p = DVec2::new(25., 25.); - - let line = Bezier::from_linear_dvec2(p, p); - let outline = line.outline::(25., Cap::Square); - assert_eq!(outline, square); - - let cubic = Bezier::from_cubic_dvec2(p, p, p, p); - let outline_cubic = cubic.outline::(25., Cap::Square); - assert_eq!(outline_cubic, square); - } - - #[test] - fn test_graduated_scale() { - let bezier = Bezier::from_linear_coordinates(30., 60., 140., 120.); - bezier.graduated_scale(10., 20.); - } - - #[test] - fn test_graduated_scale_quadratic() { - let bezier = Bezier::from_quadratic_coordinates(30., 50., 82., 98., 160., 170.); - let scaled_bezier = bezier.graduated_scale(30., 30.); - - dbg!(scaled_bezier); - - // Assert the scaled bezier is 30 units from the line - assert!(f64_compare( - scaled_bezier.evaluate(TValue::Parametric(0.)).distance(bezier.evaluate(TValue::Parametric(0.))), - 30., - MAX_ABSOLUTE_DIFFERENCE - )); - assert!(f64_compare( - scaled_bezier.evaluate(TValue::Parametric(1.)).distance(bezier.evaluate(TValue::Parametric(1.))), - 30., - MAX_ABSOLUTE_DIFFERENCE - )); - assert!(f64_compare( - scaled_bezier.evaluate(TValue::Parametric(0.5)).distance(bezier.evaluate(TValue::Parametric(0.5))), - 30., - MAX_ABSOLUTE_DIFFERENCE - )); - } - - #[test] - fn test_arcs_linear() { - let bezier = Bezier::from_linear_coordinates(30., 60., 140., 120.); - let linear_arcs = bezier.arcs(ArcsOptions::default()); - assert!(linear_arcs.is_empty()); - } - - #[test] - fn test_arcs_quadratic() { - let bezier1 = Bezier::from_quadratic_coordinates(30., 30., 50., 50., 100., 100.); - assert!(bezier1.arcs(ArcsOptions::default()).is_empty()); - - let bezier2 = Bezier::from_quadratic_coordinates(50., 50., 85., 65., 100., 100.); - let actual_arcs = bezier2.arcs(ArcsOptions::default()); - let expected_arc = CircleArc { - center: DVec2::new(15., 135.), - radius: 91.92388, - start_angle: -1.18019, - end_angle: -0.39061, - }; - assert_eq!(actual_arcs.len(), 1); - assert!(compare_arcs(actual_arcs[0], expected_arc)); - } - - #[test] - fn test_arcs_cubic() { - let bezier = Bezier::from_cubic_coordinates(30., 30., 30., 80., 60., 80., 60., 140.); - let actual_arcs = bezier.arcs(ArcsOptions::default()); - let expected_arcs = [ - CircleArc { - center: DVec2::new(122.394877, 30.7777189), - radius: 92.39815, - start_angle: 2.5637146, - end_angle: -3.1331755, - }, - CircleArc { - center: DVec2::new(-47.54881, 136.169378), - radius: 107.61701, - start_angle: -0.53556, - end_angle: 0.0356025, - }, - ]; - - assert_eq!(actual_arcs.len(), 2); - assert!(compare_arcs(actual_arcs[0], expected_arcs[0])); - assert!(compare_arcs(actual_arcs[1], expected_arcs[1])); - - // Bezier that contains the erroneous case when maximizing arcs - let bezier2 = Bezier::from_cubic_coordinates(48., 176., 170., 10., 30., 90., 180., 160.); - let auto_arcs = bezier2.arcs(ArcsOptions::default()); - - let extrema_arcs = bezier2.arcs(ArcsOptions { - strategy: ArcStrategy::FavorCorrectness, - ..ArcsOptions::default() - }); - - let maximal_arcs = bezier2.arcs(ArcsOptions { - strategy: ArcStrategy::FavorLargerArcs, - ..ArcsOptions::default() - }); - - // Resulting automatic arcs match the maximal results until the bad arc (in this case, only index 0 should match) - assert_eq!(auto_arcs[0], maximal_arcs[0]); - // Check that the first result from MaximizeArcs::Automatic should not equal the first results from MaximizeArcs::Off - assert_ne!(auto_arcs[0], extrema_arcs[0]); - // The remaining results (index 2 onwards) should match the results where MaximizeArcs::Off from the next extrema point onwards (after index 2). - assert!(auto_arcs.iter().skip(2).zip(extrema_arcs.iter().skip(2)).all(|(arc1, arc2)| compare_arcs(*arc1, *arc2))); - } -} diff --git a/libraries/bezier-rs/src/compare.rs b/libraries/bezier-rs/src/compare.rs deleted file mode 100644 index 55b6b55e91..0000000000 --- a/libraries/bezier-rs/src/compare.rs +++ /dev/null @@ -1,40 +0,0 @@ -/// Comparison functions used for tests in the bezier module -#[cfg(test)] -use super::{CircleArc, Subpath}; -use crate::consts::MAX_ABSOLUTE_DIFFERENCE; -#[cfg(test)] -use crate::utils::f64_compare; -use glam::DVec2; - -// Compare two f64s with some maximum absolute difference to account for floating point errors -#[cfg(test)] -pub fn compare_f64s(f1: f64, f2: f64) -> bool { - f64_compare(f1, f2, MAX_ABSOLUTE_DIFFERENCE) -} - -/// Compare points by allowing some maximum absolute difference to account for floating point errors -pub fn compare_points(p1: DVec2, p2: DVec2) -> bool { - p1.abs_diff_eq(p2, MAX_ABSOLUTE_DIFFERENCE) -} - -/// Compare vectors of points by allowing some maximum absolute difference to account for floating point errors -#[cfg(test)] -pub fn compare_vec_of_points(a: Vec, b: Vec, max_absolute_difference: f64) -> bool { - a.len() == b.len() && a.into_iter().zip(b).all(|(p1, p2)| p1.abs_diff_eq(p2, max_absolute_difference)) -} - -/// Compare circle arcs by allowing some maximum absolute difference between values to account for floating point errors -#[cfg(test)] -pub fn compare_arcs(arc1: CircleArc, arc2: CircleArc) -> bool { - compare_points(arc1.center, arc2.center) - && f64_compare(arc1.radius, arc1.radius, MAX_ABSOLUTE_DIFFERENCE) - && f64_compare(arc1.start_angle, arc2.start_angle, MAX_ABSOLUTE_DIFFERENCE) - && f64_compare(arc1.end_angle, arc2.end_angle, MAX_ABSOLUTE_DIFFERENCE) -} - -/// Compare Subpath by verifying that their bezier segments match. -/// In this way, matching quadratic segments where the handles are on opposite manipulator groups will be considered equal. -#[cfg(test)] -pub fn compare_subpaths(subpath1: &Subpath, subpath2: &Subpath) -> bool { - subpath1.len() == subpath2.len() && subpath1.closed() == subpath2.closed() && subpath1.iter().eq(subpath2.iter()) -} diff --git a/libraries/bezier-rs/src/consts.rs b/libraries/bezier-rs/src/consts.rs deleted file mode 100644 index bef83def97..0000000000 --- a/libraries/bezier-rs/src/consts.rs +++ /dev/null @@ -1,28 +0,0 @@ -// Implementation constants - -/// Constant used to determine if `f64`s are equivalent. -pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3; -/// A stricter constant used to determine if `f64`s are equivalent. -pub const STRICT_MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-6; -/// Maximum allowed angle that the normal of the `start` or `end` point can make with the normal of the corresponding handle for a curve to be considered scalable/simple. -pub const SCALABLE_CURVE_MAX_ENDPOINT_NORMAL_ANGLE: f64 = std::f64::consts::PI / 3.; -/// Minimum allowable separation between adjacent `t` values when calculating curve intersections -pub const MIN_SEPARATION_VALUE: f64 = 5. * 1e-3; -/// Default error bound for `t_value_to_parametric` function when TValue argument is Euclidean -pub const DEFAULT_EUCLIDEAN_ERROR_BOUND: f64 = 0.001; - -// Method argument defaults - -/// Default `t` value used for the `curve_through_points` functions. -pub const DEFAULT_T_VALUE: f64 = 0.5; -/// Default LUT step size in `compute_lookup_table` function. -pub const DEFAULT_LUT_STEP_SIZE: usize = 10; -/// Default step size for `reduce` function. -pub const DEFAULT_REDUCE_STEP_SIZE: f64 = 0.01; - -// SVG constants -pub const SVG_ARG_CUBIC: &str = "C"; -pub const SVG_ARG_LINEAR: &str = "L"; -pub const SVG_ARG_MOVE: &str = "M"; -pub const SVG_ARG_QUADRATIC: &str = "Q"; -pub const SVG_ARG_CLOSED: &str = "Z"; diff --git a/libraries/bezier-rs/src/lib.rs b/libraries/bezier-rs/src/lib.rs deleted file mode 100644 index d9442ba688..0000000000 --- a/libraries/bezier-rs/src/lib.rs +++ /dev/null @@ -1,15 +0,0 @@ -#![doc = include_str!("../README.md")] -#![allow(dead_code, unused_imports, unused_import_braces)] - -pub(crate) mod compare; - -mod bezier; -mod consts; -mod poisson_disk; -mod polynomial; -mod subpath; -mod utils; - -pub use bezier::*; -pub use subpath::*; -pub use utils::{Cap, Join, SubpathTValue, TValue, TValueType}; diff --git a/libraries/bezier-rs/src/poisson_disk.rs b/libraries/bezier-rs/src/poisson_disk.rs deleted file mode 100644 index 42cb904a8a..0000000000 --- a/libraries/bezier-rs/src/poisson_disk.rs +++ /dev/null @@ -1,369 +0,0 @@ -use core::f64; -use glam::DVec2; - -const DEEPEST_SUBDIVISION_LEVEL_BEFORE_DISCARDING: usize = 8; - -/// Fast (O(n) with respect to time and memory) algorithm for generating a maximal set of points using Poisson-disk sampling. -/// Based on the paper: -/// "Poisson Disk Point Sets by Hierarchical Dart Throwing" -/// -pub fn poisson_disk_sample( - width: f64, - height: f64, - diameter: f64, - point_in_shape_checker: impl Fn(DVec2) -> bool, - square_edges_intersect_shape_checker: impl Fn(DVec2, f64) -> bool, - rng: impl FnMut() -> f64, -) -> Vec { - let mut rng = rng; - let diameter_squared = diameter.powi(2); - - // Initialize a place to store the generated points within a spatial acceleration structure - let mut points_grid = AccelerationGrid::new(width, height, diameter); - - // Pick a grid size for the base-level domain that's as large as possible, while also: - // - Dividing into an integer number of cells across the dartboard domain, to avoid wastefully throwing darts beyond the width and height of the dartboard domain - // - Being fully covered by the radius around a dart thrown anywhere in its area, where the worst-case is a corner which has a distance of sqrt(2) to the opposite corner - let greater_dimension = width.max(height); - let base_level_grid_size = greater_dimension / (greater_dimension * std::f64::consts::SQRT_2 / (diameter / 2.)).ceil(); - - // Initialize the problem by including all base-level squares in the active list since they're all part of the yet-to-be-targetted dartboard domain - let base_level = ActiveListLevel::new_filled(base_level_grid_size, width, height, &point_in_shape_checker, &square_edges_intersect_shape_checker); - // In the future, if necessary, this could be turned into a fixed-length array with worst-case length `f64::MANTISSA_DIGITS` - let mut active_list_levels = vec![base_level]; - - // Loop until all active squares have been processed, meaning all of the dartboard domain has been checked - while active_list_levels.iter().any(|active_list| active_list.not_empty()) { - // Randomly pick a square in the dartboard domain, with probability proportional to its area - let (active_square_level, active_square_index_in_level) = target_active_square(&active_list_levels, &mut rng); - - // The level contains the list of all active squares at this target square's subdivision depth - let level = &mut active_list_levels[active_square_level]; - - // Take the targetted active square out of the list and get its size - let active_square = level.take_square(active_square_index_in_level); - let active_square_size = level.square_size(); - - // Skip this target square if it's within range of any current points, since more nearby points could have been added after this square was included in the active list - if !square_not_covered_by_poisson_points(active_square.top_left_corner(), active_square_size / 2., diameter_squared, &points_grid) { - continue; - } - - // Throw a dart by picking a random point within this target square - let point = { - let active_top_left_corner = active_square.top_left_corner(); - let x = active_top_left_corner.x + rng() * active_square_size; - let y = active_top_left_corner.y + rng() * active_square_size; - (x, y).into() - }; - - // If the dart hit a valid spot, save that point (we're now permanently done with this target square's region) - if point_not_covered_by_poisson_points(point, diameter_squared, &points_grid) { - // Silently reject the point if it lies outside the shape - if active_square.fully_in_shape() || point_in_shape_checker(point) { - points_grid.insert(point); - } - } - // Otherwise, subdivide this target square and add valid sub-squares back to the active list for later targetting - else { - // Discard any targetable domain smaller than this limited number of subdivision levels since it's too small to matter - let next_level_deeper_level = active_square_level + 1; - if next_level_deeper_level > DEEPEST_SUBDIVISION_LEVEL_BEFORE_DISCARDING { - continue; - } - - // If necessary for the following step, add another layer of depth to store squares at the next subdivision level - if active_list_levels.len() <= next_level_deeper_level { - active_list_levels.push(ActiveListLevel::new(active_square_size / 2.)) - } - - // Get the list of active squares at the level of depth beneath this target square's level - let next_level_deeper = &mut active_list_levels[next_level_deeper_level]; - - // Subdivide this target square into four sub-squares; running out of numerical precision will make this terminate at very small scales - let subdivided_size = active_square_size / 2.; - let active_top_left_corner = active_square.top_left_corner(); - let subdivided = [ - active_top_left_corner + DVec2::new(0., 0.), - active_top_left_corner + DVec2::new(subdivided_size, 0.), - active_top_left_corner + DVec2::new(0., subdivided_size), - active_top_left_corner + DVec2::new(subdivided_size, subdivided_size), - ]; - - // Add the sub-squares which aren't within the radius of a nearby point to the sub-level's active list - let half_subdivided_size = subdivided_size / 2.; - let new_sub_squares = subdivided.into_iter().filter_map(|sub_square| { - // Any sub-squares within the radius of a nearby point are filtered out - if !square_not_covered_by_poisson_points(sub_square, half_subdivided_size, diameter_squared, &points_grid) { - return None; - } - - // Fully inside the shape - if active_square.fully_in_shape() { - Some(ActiveSquare::new(sub_square, true)) - } - // Intersecting the shape's border - else { - // The sub-square is fully inside the shape if its top-left corner is inside and its edges don't intersect the shape border - let sub_square_fully_inside_shape = - !square_edges_intersect_shape_checker(sub_square, subdivided_size) && point_in_shape_checker(sub_square) && point_in_shape_checker(sub_square + subdivided_size); - // if !square_edges_intersect_shape_checker(sub_square, subdivided_size) { assert_eq!(point_in_shape_checker(sub_square), point_in_shape_checker(sub_square + subdivided_size)); } - // Sometimes this fails so it is necessary to also check the bottom right corner. - - Some(ActiveSquare::new(sub_square, sub_square_fully_inside_shape)) - } - }); - next_level_deeper.add_squares(new_sub_squares); - } - } - - points_grid.final_points() -} - -/// Randomly pick a square in the dartboard domain, with probability proportional to its area. -/// Returns a tuple with the subdivision level depth and the square index at that depth. -fn target_active_square(active_list_levels: &[ActiveListLevel], rng: &mut impl FnMut() -> f64) -> (usize, usize) { - let active_squares_total_area: f64 = active_list_levels.iter().map(|active_list| active_list.total_area()).sum(); - let mut index_into_area = rng() * active_squares_total_area; - - for (level, active_list_level) in active_list_levels.iter().enumerate() { - let subtracted = index_into_area - active_list_level.total_area(); - if subtracted > 0. { - index_into_area = subtracted; - continue; - } - - let active_square_index_in_level = (index_into_area / active_list_levels[level].square_area()).floor() as usize; - return (level, active_square_index_in_level); - } - - panic!("index_into_area couldn't be be mapped to a square in any level of the active lists"); -} - -fn point_not_covered_by_poisson_points(point: DVec2, diameter_squared: f64, points_grid: &AccelerationGrid) -> bool { - points_grid.nearby_points(point).all(|nearby_point| { - let x_separation = nearby_point.x - point.x; - let y_separation = nearby_point.y - point.y; - - x_separation.powi(2) + y_separation.powi(2) > diameter_squared - }) -} - -fn square_not_covered_by_poisson_points(point: DVec2, half_square_size: f64, diameter_squared: f64, points_grid: &AccelerationGrid) -> bool { - let square_center_x = point.x + half_square_size; - let square_center_y = point.y + half_square_size; - - points_grid.nearby_points(point).all(|nearby_point| { - let x_distance = (square_center_x - nearby_point.x).abs() + half_square_size; - let y_distance = (square_center_y - nearby_point.y).abs() + half_square_size; - - x_distance.powi(2) + y_distance.powi(2) > diameter_squared - }) -} - -#[inline(always)] -fn cartesian_product(a: A, b: B) -> impl Iterator -where - A: Iterator + Clone, - B: Iterator + Clone, - A::Item: Clone, - B::Item: Clone, -{ - a.flat_map(move |i| b.clone().map(move |j| (i.clone(), j))) -} - -/// A square (represented by its top left corner position and width/height of `square_size`) that is currently a candidate for targetting by the dart throwing process. -/// The positive sign bit encodes if the square is contained entirely within the masking shape, or negative if it's outside or intersects the shape path. -pub struct ActiveSquare(DVec2); - -impl ActiveSquare { - pub fn new(top_left_corner: DVec2, fully_in_shape: bool) -> Self { - Self(if fully_in_shape { top_left_corner } else { -top_left_corner }) - } - - pub fn top_left_corner(&self) -> DVec2 { - self.0.abs() - } - - pub fn fully_in_shape(&self) -> bool { - self.0.x.is_sign_positive() - } -} - -pub struct ActiveListLevel { - /// List of all subdivided squares of the same size that are currently candidates for targetting by the dart throwing process - active_squares: Vec, - /// Width and height of the squares in this level of subdivision - square_size: f64, - /// Current sum of the area in all active squares in this subdivision level - total_area: f64, -} - -impl ActiveListLevel { - #[inline(always)] - pub fn new(square_size: f64) -> Self { - Self { - active_squares: Vec::new(), - square_size, - total_area: 0., - } - } - - #[inline(always)] - pub fn new_filled(square_size: f64, width: f64, height: f64, point_in_shape_checker: impl Fn(DVec2) -> bool, square_edges_intersect_shape_checker: impl Fn(DVec2, f64) -> bool) -> Self { - // These should divide evenly but rounding is to protect against small numerical imprecision errors - let x_squares = (width / square_size).round() as usize; - let y_squares = (height / square_size).round() as usize; - - // Populate each square with its top-left corner coordinate - let active_squares: Vec<_> = cartesian_product(0..x_squares, 0..y_squares) - .filter_map(|(x, y)| { - let corner = (x as f64 * square_size, y as f64 * square_size).into(); - - let point_in_shape = point_in_shape_checker(corner); - let square_edges_intersect_shape = square_edges_intersect_shape_checker(corner, square_size); - let square_not_outside_shape = point_in_shape || square_edges_intersect_shape; - let square_in_shape = point_in_shape_checker(corner + square_size) && !square_edges_intersect_shape; - // if !square_edges_intersect_shape { assert_eq!(point_in_shape_checker(corner), point_in_shape_checker(corner + square_size)); } - // Sometimes this fails so it is necessary to also check the bottom right corner. - square_not_outside_shape.then_some(ActiveSquare::new(corner, square_in_shape)) - }) - .collect(); - - // Sum every square's area to get the total - let total_area = square_size.powi(2) * active_squares.len() as f64; - - Self { - active_squares, - square_size, - total_area, - } - } - - #[must_use] - #[inline(always)] - pub fn take_square(&mut self, active_square_index: usize) -> ActiveSquare { - let targetted_square = self.active_squares.swap_remove(active_square_index); - self.total_area = self.square_size.powi(2) * self.active_squares.len() as f64; - targetted_square - } - - #[inline(always)] - pub fn add_squares(&mut self, new_squares: impl Iterator) { - for new_square in new_squares { - self.active_squares.push(new_square); - } - self.total_area = self.square_size.powi(2) * self.active_squares.len() as f64; - } - - #[inline(always)] - pub fn square_size(&self) -> f64 { - self.square_size - } - - #[inline(always)] - pub fn square_area(&self) -> f64 { - self.square_size.powi(2) - } - - #[inline(always)] - pub fn total_area(&self) -> f64 { - self.total_area - } - - #[inline(always)] - pub fn not_empty(&self) -> bool { - !self.active_squares.is_empty() - } -} - -#[derive(Clone, Default)] -pub struct PointsList { - // The worst-case number of points in a 3x3 grid is 16 (one at each intersection of the four gridlines per axis) - storage_slots: [DVec2; 16], - length: usize, -} - -impl PointsList { - #[inline(always)] - pub fn push(&mut self, point: DVec2) { - self.storage_slots[self.length] = point; - self.length += 1; - } - - #[inline(always)] - pub fn list_cell_and_neighbors(&self) -> impl Iterator { - // The negative bit is used to store whether a point belongs to a neighboring cell - self.storage_slots.into_iter().take(self.length).map(|point| (point.x.abs(), point.y.abs()).into()) - } - - #[inline(always)] - pub fn list_cell(&self) -> impl Iterator { - // The negative bit is used to store whether a point belongs to a neighboring cell - self.storage_slots - .into_iter() - .take(self.length) - .filter(|point| point.x.is_sign_positive() && point.y.is_sign_positive()) - } -} - -pub struct AccelerationGrid { - size: f64, - dimension_x: usize, - dimension_y: usize, - cells: Vec, -} - -impl AccelerationGrid { - #[inline(always)] - pub fn new(width: f64, height: f64, size: f64) -> Self { - let dimension_x = (width / size).ceil() as usize + 1; - let dimension_y = (height / size).ceil() as usize + 1; - - Self { - size, - dimension_x, - dimension_y, - cells: vec![PointsList::default(); dimension_x * dimension_y], - } - } - - #[inline(always)] - pub fn insert(&mut self, point: DVec2) { - let x = (point.x / self.size).floor() as usize; - let y = (point.y / self.size).floor() as usize; - - // Insert this point at this cell and the surrounding cells in a 3x3 patch - for (x_offset, y_offset) in cartesian_product((-1)..=1, (-1)..=1) { - // Avoid going negative - let (x, y) = (x as isize + x_offset, y as isize + y_offset); - if x < 0 || y < 0 { - continue; - } - // Avoid going beyond the width or height - let (x, y) = (x as usize, y as usize); - if x > self.dimension_x - 1 || y > self.dimension_y - 1 { - continue; - } - - // Get the cell corresponding to the (x, y) index - let cell = &mut self.cells[y * self.dimension_x + x]; - - // Store the given point in this grid cell, and use the negative bit to indicate if this belongs to a neighboring cell - cell.push(if x_offset == 0 && y_offset == 0 { point } else { -point }); - } - } - - #[inline(always)] - pub fn nearby_points(&self, point: DVec2) -> impl Iterator { - let x = (point.x / self.size).floor() as usize; - let y = (point.y / self.size).floor() as usize; - - self.cells[y * self.dimension_x + x].list_cell_and_neighbors() - } - - #[inline(always)] - pub fn final_points(&self) -> Vec { - self.cells.iter().flat_map(|cell| cell.list_cell()).collect() - } -} diff --git a/libraries/bezier-rs/src/polynomial.rs b/libraries/bezier-rs/src/polynomial.rs deleted file mode 100644 index 1deee9ce97..0000000000 --- a/libraries/bezier-rs/src/polynomial.rs +++ /dev/null @@ -1,264 +0,0 @@ -use std::fmt::{self, Display, Formatter}; -use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; - -/// A struct that represents a polynomial with a maximum degree of `N-1`. -/// -/// It provides basic mathematical operations for polynomials like addition, multiplication, differentiation, integration, etc. -#[derive(Copy, Clone, Debug, PartialEq)] -pub struct Polynomial { - coefficients: [f64; N], -} - -impl Polynomial { - /// Create a new polynomial from the coefficients given in the array. - /// - /// The coefficient for nth degree is at the nth index in array. Therefore the order of coefficients are reversed than the usual order for writing polynomials mathematically. - pub fn new(coefficients: [f64; N]) -> Polynomial { - Polynomial { coefficients } - } - - /// Create a polynomial where all its coefficients are zero. - pub fn zero() -> Polynomial { - Polynomial { coefficients: [0.; N] } - } - - /// Return an immutable reference to the coefficients. - /// - /// The coefficient for nth degree is at the nth index in array. Therefore the order of coefficients are reversed than the usual order for writing polynomials mathematically. - pub fn coefficients(&self) -> &[f64; N] { - &self.coefficients - } - - /// Return a mutable reference to the coefficients. - /// - /// The coefficient for nth degree is at the nth index in array. Therefore the order of coefficients are reversed than the usual order for writing polynomials mathematically. - pub fn coefficients_mut(&mut self) -> &mut [f64; N] { - &mut self.coefficients - } - - /// Evaluate the polynomial at `value`. - pub fn eval(&self, value: f64) -> f64 { - self.coefficients.iter().rev().copied().reduce(|acc, x| acc * value + x).unwrap() - } - - /// Return the same polynomial but with a different maximum degree of `M-1`.\ - /// - /// Returns `None` if the polynomial cannot fit in the specified size. - pub fn as_size(&self) -> Option> { - let mut coefficients = [0.; M]; - - if M >= N { - coefficients[..N].copy_from_slice(&self.coefficients); - } else if self.coefficients.iter().rev().take(N - M).all(|&x| x == 0.) { - coefficients.copy_from_slice(&self.coefficients[..M]) - } else { - return None; - } - - Some(Polynomial { coefficients }) - } - - /// Computes the derivative in place. - pub fn derivative_mut(&mut self) { - self.coefficients.iter_mut().enumerate().for_each(|(index, x)| *x *= index as f64); - self.coefficients.rotate_left(1); - } - - /// Computes the antiderivative at `C = 0` in place. - /// - /// Returns `None` if the polynomial is not big enough to accommodate the extra degree. - pub fn antiderivative_mut(&mut self) -> Option<()> { - if self.coefficients[N - 1] != 0. { - return None; - } - self.coefficients.rotate_right(1); - self.coefficients.iter_mut().enumerate().skip(1).for_each(|(index, x)| *x /= index as f64); - Some(()) - } - - /// Computes the polynomial's derivative. - pub fn derivative(&self) -> Polynomial { - let mut ans = *self; - ans.derivative_mut(); - ans - } - - /// Computes the antiderivative at `C = 0`. - /// - /// Returns `None` if the polynomial is not big enough to accommodate the extra degree. - pub fn antiderivative(&self) -> Option> { - let mut ans = *self; - ans.antiderivative_mut()?; - Some(ans) - } -} - -impl Default for Polynomial { - fn default() -> Self { - Self::zero() - } -} - -impl Display for Polynomial { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let mut first = true; - for (index, coefficient) in self.coefficients.iter().enumerate().rev().filter(|&(_, &coefficient)| coefficient != 0.) { - if first { - first = false; - } else { - f.write_str(" + ")? - } - - coefficient.fmt(f)?; - if index == 0 { - continue; - } - f.write_str("x")?; - if index == 1 { - continue; - } - f.write_str("^")?; - index.fmt(f)?; - } - - Ok(()) - } -} - -impl AddAssign<&Polynomial> for Polynomial { - fn add_assign(&mut self, rhs: &Polynomial) { - self.coefficients.iter_mut().zip(rhs.coefficients.iter()).for_each(|(a, b)| *a += b); - } -} - -impl Add for &Polynomial { - type Output = Polynomial; - - fn add(self, other: &Polynomial) -> Polynomial { - let mut output = *self; - output += other; - output - } -} - -impl Neg for &Polynomial { - type Output = Polynomial; - - fn neg(self) -> Polynomial { - let mut output = *self; - output.coefficients.iter_mut().for_each(|x| *x = -*x); - output - } -} - -impl Neg for Polynomial { - type Output = Polynomial; - - fn neg(mut self) -> Polynomial { - self.coefficients.iter_mut().for_each(|x| *x = -*x); - self - } -} - -impl SubAssign<&Polynomial> for Polynomial { - fn sub_assign(&mut self, rhs: &Polynomial) { - self.coefficients.iter_mut().zip(rhs.coefficients.iter()).for_each(|(a, b)| *a -= b); - } -} - -impl Sub for &Polynomial { - type Output = Polynomial; - - fn sub(self, other: &Polynomial) -> Polynomial { - let mut output = *self; - output -= other; - output - } -} - -impl MulAssign<&Polynomial> for Polynomial { - fn mul_assign(&mut self, rhs: &Polynomial) { - for i in (0..N).rev() { - self.coefficients[i] = self.coefficients[i] * rhs.coefficients[0]; - for j in 0..i { - self.coefficients[i] += self.coefficients[j] * rhs.coefficients[i - j]; - } - } - } -} - -impl Mul for &Polynomial { - type Output = Polynomial; - - fn mul(self, other: &Polynomial) -> Polynomial { - let mut output = *self; - output *= other; - output - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn evaluation() { - let p = Polynomial::new([1., 2., 3.]); - - assert_eq!(p.eval(1.), 6.); - assert_eq!(p.eval(2.), 17.); - } - - #[test] - fn size_change() { - let p1 = Polynomial::new([1., 2., 3.]); - let p2 = Polynomial::new([1., 2., 3., 0.]); - - assert_eq!(p1.as_size(), Some(p2)); - assert_eq!(p2.as_size(), Some(p1)); - - assert_eq!(p2.as_size::<2>(), None); - } - - #[test] - fn addition_and_subtaction() { - let p1 = Polynomial::new([1., 2., 3.]); - let p2 = Polynomial::new([4., 5., 6.]); - - let addition = Polynomial::new([5., 7., 9.]); - let subtraction = Polynomial::new([-3., -3., -3.]); - - assert_eq!(&p1 + &p2, addition); - assert_eq!(&p1 - &p2, subtraction); - } - - #[test] - fn multiplication() { - let p1 = Polynomial::new([1., 2., 3.]).as_size().unwrap(); - let p2 = Polynomial::new([4., 5., 6.]).as_size().unwrap(); - - let multiplication = Polynomial::new([4., 13., 28., 27., 18.]); - - assert_eq!(&p1 * &p2, multiplication); - } - - #[test] - fn derivative_and_antiderivative() { - let mut p = Polynomial::new([1., 2., 3.]); - let p_deriv = Polynomial::new([2., 6., 0.]); - - assert_eq!(p.derivative(), p_deriv); - - p.coefficients_mut()[0] = 0.; - assert_eq!(p_deriv.antiderivative().unwrap(), p); - - assert_eq!(p.antiderivative(), None); - } - - #[test] - fn display() { - let p = Polynomial::new([1., 2., 0., 3.]); - - assert_eq!(format!("{:.2}", p), "3.00x^3 + 2.00x + 1.00"); - } -} diff --git a/libraries/bezier-rs/src/subpath/core.rs b/libraries/bezier-rs/src/subpath/core.rs deleted file mode 100644 index a18550db6d..0000000000 --- a/libraries/bezier-rs/src/subpath/core.rs +++ /dev/null @@ -1,602 +0,0 @@ -use super::*; -use crate::consts::*; -use crate::utils::format_point; -use glam::DVec2; -use std::fmt::Write; - -/// Functionality relating to core `Subpath` operations, such as constructors and `iter`. -impl Subpath { - /// Create a new `Subpath` using a list of [ManipulatorGroup]s. - /// A `Subpath` with less than 2 [ManipulatorGroup]s may not be closed. - #[track_caller] - pub fn new(manipulator_groups: Vec>, closed: bool) -> Self { - assert!(!closed || !manipulator_groups.is_empty(), "A closed Subpath must contain more than 0 ManipulatorGroups."); - Self { manipulator_groups, closed } - } - - /// Create a `Subpath` consisting of 2 manipulator groups from a `Bezier`. - pub fn from_bezier(bezier: &Bezier) -> Self { - Subpath::new( - vec![ - ManipulatorGroup { - anchor: bezier.start(), - in_handle: None, - out_handle: bezier.handle_start(), - id: PointId::new(), - }, - ManipulatorGroup { - anchor: bezier.end(), - in_handle: bezier.handle_end(), - out_handle: None, - id: PointId::new(), - }, - ], - false, - ) - } - - /// Creates a subpath from a slice of [Bezier]. When two consecutive Beziers do not share an end and start point, this function - /// resolves the discrepancy by simply taking the start-point of the second Bezier as the anchor of the Manipulator Group. - pub fn from_beziers(beziers: &[Bezier], closed: bool) -> Self { - assert!(!closed || beziers.len() > 1, "A closed Subpath must contain at least 1 Bezier."); - if beziers.is_empty() { - return Subpath::new(vec![], closed); - } - - let first = beziers.first().unwrap(); - let mut manipulator_groups = vec![ManipulatorGroup { - anchor: first.start(), - in_handle: None, - out_handle: first.handle_start(), - id: PointId::new(), - }]; - let mut inner_groups: Vec> = beziers - .windows(2) - .map(|bezier_pair| ManipulatorGroup { - anchor: bezier_pair[1].start(), - in_handle: bezier_pair[0].handle_end(), - out_handle: bezier_pair[1].handle_start(), - id: PointId::new(), - }) - .collect::>>(); - manipulator_groups.append(&mut inner_groups); - - let last = beziers.last().unwrap(); - if !closed { - manipulator_groups.push(ManipulatorGroup { - anchor: last.end(), - in_handle: last.handle_end(), - out_handle: None, - id: PointId::new(), - }); - return Subpath::new(manipulator_groups, false); - } - - manipulator_groups[0].in_handle = last.handle_end(); - Subpath::new(manipulator_groups, true) - } - - /// Returns true if the `Subpath` contains no [ManipulatorGroup]. - pub fn is_empty(&self) -> bool { - self.manipulator_groups.is_empty() - } - - /// Returns the number of [ManipulatorGroup]s contained within the `Subpath`. - pub fn len(&self) -> usize { - self.manipulator_groups.len() - } - - /// Returns the number of segments contained within the `Subpath`. - pub fn len_segments(&self) -> usize { - let mut number_of_curves = self.len(); - if !self.closed && number_of_curves > 0 { - number_of_curves -= 1 - } - number_of_curves - } - - /// Returns a copy of the bezier segment at the given segment index, if this segment exists. - pub fn get_segment(&self, segment_index: usize) -> Option { - if segment_index >= self.len_segments() { - return None; - } - Some(self[segment_index].to_bezier(&self[(segment_index + 1) % self.len()])) - } - - /// Returns an iterator of the [Bezier]s along the `Subpath`. - pub fn iter(&self) -> SubpathIter<'_, PointId> { - SubpathIter { - subpath: self, - index: 0, - is_always_closed: false, - } - } - - /// Returns an iterator of the [Bezier]s along the `Subpath` always considering it as a closed subpath. - pub fn iter_closed(&self) -> SubpathIter<'_, PointId> { - SubpathIter { - subpath: self, - index: 0, - is_always_closed: true, - } - } - - /// Returns a slice of the [ManipulatorGroup]s in the `Subpath`. - pub fn manipulator_groups(&self) -> &[ManipulatorGroup] { - &self.manipulator_groups - } - - /// Returns a mutable reference to the [ManipulatorGroup]s in the `Subpath`. - pub fn manipulator_groups_mut(&mut self) -> &mut Vec> { - &mut self.manipulator_groups - } - - /// Returns a vector of all the anchors (DVec2) for this `Subpath`. - pub fn anchors(&self) -> Vec { - self.manipulator_groups().iter().map(|group| group.anchor).collect() - } - - /// Returns if the Subpath is equivalent to a single point. - pub fn is_point(&self) -> bool { - if self.is_empty() { - return false; - } - let point = self.manipulator_groups[0].anchor; - self.manipulator_groups - .iter() - .all(|manipulator_group| manipulator_group.anchor.abs_diff_eq(point, MAX_ABSOLUTE_DIFFERENCE)) - } - - /// Appends to the `svg` mutable string with an SVG shape representation of the curve. - pub fn curve_to_svg(&self, svg: &mut String, attributes: String) { - let curve_start_argument = format!("{SVG_ARG_MOVE}{} {}", self[0].anchor.x, self[0].anchor.y); - let mut curve_arguments: Vec = self.iter().map(|bezier| bezier.svg_curve_argument()).collect(); - if self.closed { - curve_arguments.push(String::from(SVG_ARG_CLOSED)); - } - - let _ = write!(svg, r#""#, curve_start_argument, curve_arguments.join(" ")); - } - - /// Write the curve argument to the string (the d="..." part) - pub fn subpath_to_svg(&self, svg: &mut String, transform: glam::DAffine2) -> std::fmt::Result { - if self.is_empty() { - return Ok(()); - } - - let start = transform.transform_point2(self[0].anchor); - format_point(svg, SVG_ARG_MOVE, start.x, start.y)?; - - for bezier in self.iter() { - bezier.apply_transformation(|pos| transform.transform_point2(pos)).write_curve_argument(svg)?; - svg.push(' '); - } - if self.closed { - svg.push_str(SVG_ARG_CLOSED); - } - Ok(()) - } - - /// Appends to the `svg` mutable string with an SVG shape representation of the handle lines. - pub fn handle_lines_to_svg(&self, svg: &mut String, attributes: String) { - let handle_lines: Vec = self.iter().filter_map(|bezier| bezier.svg_handle_line_argument()).collect(); - let _ = write!(svg, r#""#, handle_lines.join(" ")); - } - - /// Appends to the `svg` mutable string with an SVG shape representation of the anchors. - pub fn anchors_to_svg(&self, svg: &mut String, attributes: String) { - let anchors = self - .manipulator_groups - .iter() - .map(|point| format!(r#""#, point.anchor.x, point.anchor.y)) - .collect::>(); - let _ = write!(svg, "{}", anchors.concat()); - } - - /// Appends to the `svg` mutable string with an SVG shape representation of the handles. - pub fn handles_to_svg(&self, svg: &mut String, attributes: String) { - let handles = self - .manipulator_groups - .iter() - .flat_map(|group| [group.in_handle, group.out_handle]) - .flatten() - .map(|handle| format!(r#""#, handle.x, handle.y)) - .collect::>(); - let _ = write!(svg, "{}", handles.concat()); - } - - /// Returns an SVG representation of the `Subpath`. - /// Appends to the `svg` mutable string with an SVG shape representation that includes the curve, the handle lines, the anchors, and the handles. - pub fn to_svg(&self, svg: &mut String, curve_attributes: String, anchor_attributes: String, handle_attributes: String, handle_line_attributes: String) { - if !curve_attributes.is_empty() { - self.curve_to_svg(svg, curve_attributes); - } - if !handle_line_attributes.is_empty() { - self.handle_lines_to_svg(svg, handle_line_attributes); - } - if !anchor_attributes.is_empty() { - self.anchors_to_svg(svg, anchor_attributes); - } - if !handle_attributes.is_empty() { - self.handles_to_svg(svg, handle_attributes); - } - } - - /// Construct a [Subpath] from an iter of anchor positions. - pub fn from_anchors(anchor_positions: impl IntoIterator, closed: bool) -> Self { - Self::new(anchor_positions.into_iter().map(|anchor| ManipulatorGroup::new_anchor(anchor)).collect(), closed) - } - - pub fn from_anchors_linear(anchor_positions: impl IntoIterator, closed: bool) -> Self { - Self::new(anchor_positions.into_iter().map(|anchor| ManipulatorGroup::new_anchor_linear(anchor)).collect(), closed) - } - - /// Constructs a rectangle with `corner1` and `corner2` as the two corners. - pub fn new_rect(corner1: DVec2, corner2: DVec2) -> Self { - Self::from_anchors_linear([corner1, DVec2::new(corner2.x, corner1.y), corner2, DVec2::new(corner1.x, corner2.y)], true) - } - - /// Constructs a rounded rectangle with `corner1` and `corner2` as the two corners and `corner_radii` as the radii of the corners: `[top_left, top_right, bottom_right, bottom_left]`. - pub fn new_rounded_rect(corner1: DVec2, corner2: DVec2, corner_radii: [f64; 4]) -> Self { - if corner_radii.iter().all(|radii| radii.abs() < f64::EPSILON * 100.) { - return Self::new_rect(corner1, corner2); - } - - use std::f64::consts::{FRAC_1_SQRT_2, PI}; - - let new_arc = |center: DVec2, corner: DVec2, radius: f64| -> Vec> { - let point1 = center + DVec2::from_angle(-PI * 0.25).rotate(corner - center) * FRAC_1_SQRT_2; - let point2 = center + DVec2::from_angle(PI * 0.25).rotate(corner - center) * FRAC_1_SQRT_2; - if radius == 0. { - return vec![ManipulatorGroup::new_anchor(point1), ManipulatorGroup::new_anchor(point2)]; - } - - // Based on https://pomax.github.io/bezierinfo/#circles_cubic - const HANDLE_OFFSET_FACTOR: f64 = 0.551784777779014; - let handle_offset = radius * HANDLE_OFFSET_FACTOR; - vec![ - ManipulatorGroup::new(point1, None, Some(point1 + handle_offset * (corner - point1).normalize())), - ManipulatorGroup::new(point2, Some(point2 + handle_offset * (corner - point2).normalize()), None), - ] - }; - Self::new( - [ - new_arc(DVec2::new(corner1.x + corner_radii[0], corner1.y + corner_radii[0]), DVec2::new(corner1.x, corner1.y), corner_radii[0]), - new_arc(DVec2::new(corner2.x - corner_radii[1], corner1.y + corner_radii[1]), DVec2::new(corner2.x, corner1.y), corner_radii[1]), - new_arc(DVec2::new(corner2.x - corner_radii[2], corner2.y - corner_radii[2]), DVec2::new(corner2.x, corner2.y), corner_radii[2]), - new_arc(DVec2::new(corner1.x + corner_radii[3], corner2.y - corner_radii[3]), DVec2::new(corner1.x, corner2.y), corner_radii[3]), - ] - .concat(), - true, - ) - } - - /// Constructs an ellipse with `corner1` and `corner2` as the two corners of the bounding box. - pub fn new_ellipse(corner1: DVec2, corner2: DVec2) -> Self { - let size = (corner1 - corner2).abs(); - let center = (corner1 + corner2) / 2.; - let top = DVec2::new(center.x, corner1.y); - let bottom = DVec2::new(center.x, corner2.y); - let left = DVec2::new(corner1.x, center.y); - let right = DVec2::new(corner2.x, center.y); - - // Based on https://pomax.github.io/bezierinfo/#circles_cubic - const HANDLE_OFFSET_FACTOR: f64 = 0.551784777779014; - let handle_offset = size * HANDLE_OFFSET_FACTOR * 0.5; - - let manipulator_groups = vec![ - ManipulatorGroup::new(top, Some(top - handle_offset * DVec2::X), Some(top + handle_offset * DVec2::X)), - ManipulatorGroup::new(right, Some(right - handle_offset * DVec2::Y), Some(right + handle_offset * DVec2::Y)), - ManipulatorGroup::new(bottom, Some(bottom + handle_offset * DVec2::X), Some(bottom - handle_offset * DVec2::X)), - ManipulatorGroup::new(left, Some(left + handle_offset * DVec2::Y), Some(left - handle_offset * DVec2::Y)), - ]; - Self::new(manipulator_groups, true) - } - - /// Constructs an arc by a `radius`, `angle_start` and `angle_size`. Angles must be in radians. Slice option makes it look like pie or pacman. - pub fn new_arc(radius: f64, start_angle: f64, sweep_angle: f64, arc_type: ArcType) -> Self { - // Prevents glitches from numerical imprecision that have been observed during animation playback after about a minute - let start_angle = start_angle % (std::f64::consts::TAU * 2.); - let sweep_angle = sweep_angle % (std::f64::consts::TAU * 2.); - - let original_start_angle = start_angle; - let sweep_angle_sign = sweep_angle.signum(); - - let mut start_angle = 0.; - let mut sweep_angle = sweep_angle.abs(); - - if (sweep_angle / std::f64::consts::TAU).floor() as u32 % 2 == 0 { - sweep_angle %= std::f64::consts::TAU; - } else { - start_angle = sweep_angle % std::f64::consts::TAU; - sweep_angle = std::f64::consts::TAU - start_angle; - } - - sweep_angle *= sweep_angle_sign; - start_angle *= sweep_angle_sign; - start_angle += original_start_angle; - - let closed = arc_type == ArcType::Closed; - let slice = arc_type == ArcType::PieSlice; - - let center = DVec2::new(0., 0.); - let segments = (sweep_angle.abs() / (std::f64::consts::PI / 4.)).ceil().max(1.) as usize; - let step = sweep_angle / segments as f64; - let factor = 4. / 3. * (step / 2.).sin() / (1. + (step / 2.).cos()); - - let mut manipulator_groups = Vec::with_capacity(segments); - let mut prev_in_handle = None; - let mut prev_end = DVec2::new(0., 0.); - - for i in 0..segments { - let start_angle = start_angle + step * i as f64; - let end_angle = start_angle + step; - let start_vec = DVec2::from_angle(start_angle); - let end_vec = DVec2::from_angle(end_angle); - - let start = center + radius * start_vec; - let end = center + radius * end_vec; - - let handle_start = start + start_vec.perp() * radius * factor; - let handle_end = end - end_vec.perp() * radius * factor; - - manipulator_groups.push(ManipulatorGroup::new(start, prev_in_handle, Some(handle_start))); - prev_in_handle = Some(handle_end); - prev_end = end; - } - manipulator_groups.push(ManipulatorGroup::new(prev_end, prev_in_handle, None)); - - if slice { - manipulator_groups.push(ManipulatorGroup::new(center, None, None)); - } - - Self::new(manipulator_groups, closed || slice) - } - - /// Constructs a regular polygon (ngon). Based on `sides` and `radius`, which is the distance from the center to any vertex. - pub fn new_regular_polygon(center: DVec2, sides: u64, radius: f64) -> Self { - let sides = sides.max(3); - let angle_increment = std::f64::consts::TAU / (sides as f64); - let anchor_positions = (0..sides).map(|i| { - let angle = (i as f64) * angle_increment - std::f64::consts::FRAC_PI_2; - let center = center + DVec2::ONE * radius; - DVec2::new(center.x + radius * f64::cos(angle), center.y + radius * f64::sin(angle)) * 0.5 - }); - Self::from_anchors(anchor_positions, true) - } - - /// Constructs a star polygon (n-star). See [new_regular_polygon], but with interspersed vertices at an `inner_radius`. - pub fn new_star_polygon(center: DVec2, sides: u64, radius: f64, inner_radius: f64) -> Self { - let sides = sides.max(2); - let angle_increment = 0.5 * std::f64::consts::TAU / (sides as f64); - let anchor_positions = (0..sides * 2).map(|i| { - let angle = (i as f64) * angle_increment - std::f64::consts::FRAC_PI_2; - let center = center + DVec2::ONE * radius; - let r = if i % 2 == 0 { radius } else { inner_radius }; - DVec2::new(center.x + r * f64::cos(angle), center.y + r * f64::sin(angle)) * 0.5 - }); - Self::from_anchors(anchor_positions, true) - } - - /// Constructs a line from `p1` to `p2` - pub fn new_line(p1: DVec2, p2: DVec2) -> Self { - Self::from_anchors([p1, p2], false) - } - - /// Construct a cubic spline from a list of points. - /// Based on . - pub fn new_cubic_spline(points: Vec) -> Self { - if points.len() < 2 { - return Self::new(Vec::new(), false); - } - - // Number of points = number of points to find handles for - let len_points = points.len(); - - let out_handles = solve_spline_first_handle_open(&points); - - let mut subpath = Subpath::new(Vec::new(), false); - - // given the second point in the n'th cubic bezier, the third point is given by 2 * points[n+1] - b[n+1]. - // to find 'handle1_pos' for the n'th point we need the n-1 cubic bezier - subpath.manipulator_groups.push(ManipulatorGroup::new(points[0], None, Some(out_handles[0]))); - for i in 1..len_points - 1 { - subpath - .manipulator_groups - .push(ManipulatorGroup::new(points[i], Some(2. * points[i] - out_handles[i]), Some(out_handles[i]))); - } - subpath - .manipulator_groups - .push(ManipulatorGroup::new(points[len_points - 1], Some(2. * points[len_points - 1] - out_handles[len_points - 1]), None)); - - subpath - } - - #[cfg(feature = "kurbo")] - pub fn to_vello_path(&self, transform: glam::DAffine2, path: &mut kurbo::BezPath) { - use crate::BezierHandles; - - let to_point = |p: DVec2| { - let p = transform.transform_point2(p); - kurbo::Point::new(p.x, p.y) - }; - path.move_to(to_point(self.iter().next().unwrap().start)); - for segment in self.iter() { - match segment.handles { - BezierHandles::Linear => path.line_to(to_point(segment.end)), - BezierHandles::Quadratic { handle } => path.quad_to(to_point(handle), to_point(segment.end)), - BezierHandles::Cubic { handle_start, handle_end } => path.curve_to(to_point(handle_start), to_point(handle_end), to_point(segment.end)), - } - } - if self.closed { - path.close_path(); - } - } -} - -/// Solve for the first handle of an open spline. (The opposite handle can be found by mirroring the result about the anchor.) -pub fn solve_spline_first_handle_open(points: &[DVec2]) -> Vec { - let len_points = points.len(); - if len_points == 0 { - return Vec::new(); - } - if len_points == 1 { - return vec![points[0]]; - } - - // Matrix coefficients a, b and c (see https://mathworld.wolfram.com/CubicSpline.html). - // Because the `a` coefficients are all 1, they need not be stored. - // This algorithm does a variation of the above algorithm. - // Instead of using the traditional cubic (a + bt + ct^2 + dt^3), we use the bezier cubic. - - let mut b = vec![DVec2::new(4., 4.); len_points]; - b[0] = DVec2::new(2., 2.); - b[len_points - 1] = DVec2::new(2., 2.); - - let mut c = vec![DVec2::new(1., 1.); len_points]; - - // 'd' is the the second point in a cubic bezier, which is what we solve for - let mut d = vec![DVec2::ZERO; len_points]; - - d[0] = DVec2::new(2. * points[1].x + points[0].x, 2. * points[1].y + points[0].y); - d[len_points - 1] = DVec2::new(3. * points[len_points - 1].x, 3. * points[len_points - 1].y); - for idx in 1..(len_points - 1) { - d[idx] = DVec2::new(4. * points[idx].x + 2. * points[idx + 1].x, 4. * points[idx].y + 2. * points[idx + 1].y); - } - - // Solve with Thomas algorithm (see https://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm) - // Now we do row operations to eliminate `a` coefficients. - c[0] /= -b[0]; - d[0] /= -b[0]; - #[allow(clippy::assign_op_pattern)] - for i in 1..len_points { - b[i] += c[i - 1]; - // For some reason this `+=` version makes the borrow checker mad: - // d[i] += d[i-1] - d[i] = d[i] + d[i - 1]; - c[i] /= -b[i]; - d[i] /= -b[i]; - } - - // At this point b[i] == -a[i + 1] and a[i] == 0. - // Now we do row operations to eliminate 'c' coefficients and solve. - d[len_points - 1] *= -1.; - #[allow(clippy::assign_op_pattern)] - for i in (0..len_points - 1).rev() { - d[i] = d[i] - (c[i] * d[i + 1]); - d[i] *= -1.; // d[i] /= b[i] - } - - d -} - -/// Solve for the first handle of a closed spline. (The opposite handle can be found by mirroring the result about the anchor.) -/// If called with fewer than 3 points, this function will return an empty result. -pub fn solve_spline_first_handle_closed(points: &[DVec2]) -> Vec { - let len_points = points.len(); - if len_points < 3 { - return Vec::new(); - } - - // Matrix coefficients `a`, `b` and `c` (see https://mathworld.wolfram.com/CubicSpline.html). - // We don't really need to allocate them but it keeps the maths understandable. - let a = vec![DVec2::ONE; len_points]; - let b = vec![DVec2::splat(4.); len_points]; - let c = vec![DVec2::ONE; len_points]; - - let mut cmod = vec![DVec2::ZERO; len_points]; - let mut u = vec![DVec2::ZERO; len_points]; - - // `x` is initially the output of the matrix multiplication, but is converted to the second value. - let mut x = vec![DVec2::ZERO; len_points]; - - for (i, point) in x.iter_mut().enumerate() { - let previous_i = i.checked_sub(1).unwrap_or(len_points - 1); - let next_i = (i + 1) % len_points; - *point = 3. * (points[next_i] - points[previous_i]); - } - - // Solve using https://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm#Variants (the variant using periodic boundary conditions). - // This code below is based on the reference C language implementation provided in that section of the article. - let alpha = a[0]; - let beta = c[len_points - 1]; - - // Arbitrary, but chosen such that division by zero is avoided. - let gamma = -b[0]; - - cmod[0] = alpha / (b[0] - gamma); - u[0] = gamma / (b[0] - gamma); - x[0] /= b[0] - gamma; - - // Handle from from `1` to `len_points - 2` (inclusive). - for ix in 1..=(len_points - 2) { - let m = 1.0 / (b[ix] - a[ix] * cmod[ix - 1]); - cmod[ix] = c[ix] * m; - u[ix] = (0.0 - a[ix] * u[ix - 1]) * m; - x[ix] = (x[ix] - a[ix] * x[ix - 1]) * m; - } - - // Handle `len_points - 1`. - let m = 1.0 / (b[len_points - 1] - alpha * beta / gamma - beta * cmod[len_points - 2]); - u[len_points - 1] = (alpha - a[len_points - 1] * u[len_points - 2]) * m; - x[len_points - 1] = (x[len_points - 1] - a[len_points - 1] * x[len_points - 2]) * m; - - // Loop from `len_points - 2` to `0` (inclusive). - for ix in (0..=(len_points - 2)).rev() { - u[ix] = u[ix] - cmod[ix] * u[ix + 1]; - x[ix] = x[ix] - cmod[ix] * x[ix + 1]; - } - - let fact = (x[0] + x[len_points - 1] * beta / gamma) / (1.0 + u[0] + u[len_points - 1] * beta / gamma); - - for ix in 0..(len_points) { - x[ix] -= fact * u[ix]; - } - - let mut real = vec![DVec2::ZERO; len_points]; - for i in 0..len_points { - let previous = i.checked_sub(1).unwrap_or(len_points - 1); - let next = (i + 1) % len_points; - real[i] = x[previous] * a[next] + x[i] * b[i] + x[next] * c[i]; - } - - // The matrix is now solved. - - // Since we have computed the derivative, work back to find the start handle. - for i in 0..len_points { - x[i] = (x[i] / 3.) + points[i]; - } - - x -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn closed_spline() { - // These points are just chosen arbitrary - let points = [DVec2::new(0., 0.), DVec2::new(0., 0.), DVec2::new(6., 5.), DVec2::new(7., 9.), DVec2::new(2., 3.)]; - - let out_handles = solve_spline_first_handle_closed(&points); - - // Construct the Subpath - let mut manipulator_groups = Vec::new(); - for i in 0..out_handles.len() { - manipulator_groups.push(ManipulatorGroup::::new(points[i], Some(2. * points[i] - out_handles[i]), Some(out_handles[i]))); - } - let subpath = Subpath::new(manipulator_groups, true); - - // For each pair of bézier curves, ensure that the second derivative is continuous - for (bézier_a, bézier_b) in subpath.iter().zip(subpath.iter().skip(1).chain(subpath.iter().take(1))) { - let derivative2_end_a = bézier_a.derivative().unwrap().derivative().unwrap().evaluate(crate::TValue::Parametric(1.)); - let derivative2_start_b = bézier_b.derivative().unwrap().derivative().unwrap().evaluate(crate::TValue::Parametric(0.)); - - assert!( - derivative2_end_a.abs_diff_eq(derivative2_start_b, 1e-10), - "second derivative at the end of a {derivative2_end_a} is equal to the second derivative at the start of b {derivative2_start_b}" - ); - } - } -} diff --git a/libraries/bezier-rs/src/subpath/lookup.rs b/libraries/bezier-rs/src/subpath/lookup.rs deleted file mode 100644 index a63b7a179a..0000000000 --- a/libraries/bezier-rs/src/subpath/lookup.rs +++ /dev/null @@ -1,519 +0,0 @@ -use super::*; -use crate::consts::{DEFAULT_EUCLIDEAN_ERROR_BOUND, DEFAULT_LUT_STEP_SIZE, MAX_ABSOLUTE_DIFFERENCE}; -use crate::utils::{SubpathTValue, TValue, TValueType}; -use glam::DVec2; - -/// Functionality relating to looking up properties of the `Subpath` or points along the `Subpath`. -impl Subpath { - /// Return a selection of equidistant points on the bezier curve. - /// If no value is provided for `steps`, then the function will default `steps` to be 10. - /// - pub fn compute_lookup_table(&self, steps: Option, tvalue_type: Option) -> Vec { - let steps = steps.unwrap_or(DEFAULT_LUT_STEP_SIZE); - let tvalue_type = tvalue_type.unwrap_or(TValueType::Parametric); - - (0..=steps) - .map(|t| { - let tvalue = match tvalue_type { - TValueType::Parametric => SubpathTValue::GlobalParametric(t as f64 / steps as f64), - TValueType::Euclidean => SubpathTValue::GlobalEuclidean(t as f64 / steps as f64), - }; - self.evaluate(tvalue) - }) - .collect() - } - - /// Return the sum of the approximation of the length of each `Bezier` curve along the `Subpath`. - /// - `tolerance` - Tolerance used to approximate the curve. - /// - pub fn length(&self, tolerance: Option) -> f64 { - self.iter().map(|bezier| bezier.length(tolerance)).sum() - } - - /// Return the approximation of the length centroid, together with the length, of the `Subpath`. - /// - /// The length centroid is the center of mass for the arc length of the solid shape's perimeter. - /// An infinitely thin wire forming the subpath's closed shape would balance at this point. - /// - /// It will return `None` if no manipulator is present. - /// - `tolerance` - Tolerance used to approximate the curve. - /// - `always_closed` - consider the subpath as closed always. - pub fn length_centroid_and_length(&self, tolerance: Option, always_closed: bool) -> Option<(DVec2, f64)> { - if always_closed { self.iter_closed() } else { self.iter() } - .map(|bezier| bezier.length_centroid_and_length(tolerance)) - .map(|(centroid, length)| (centroid * length, length)) - .reduce(|(centroid_part1, length1), (centroid_part2, length2)| (centroid_part1 + centroid_part2, length1 + length2)) - .map(|(centroid_part, length)| (centroid_part / length, length)) - } - - /// Return the approximation of the length centroid of the `Subpath`. - /// - /// The length centroid is the center of mass for the arc length of the solid shape's perimeter. - /// An infinitely thin wire forming the subpath's closed shape would balance at this point. - /// - /// It will return `None` if no manipulator is present. - /// - `tolerance` - Tolerance used to approximate the curve. - /// - `always_closed` - consider the subpath as closed always. - /// - pub fn length_centroid(&self, tolerance: Option, always_closed: bool) -> Option { - self.length_centroid_and_length(tolerance, always_closed).map(|(centroid, _)| centroid) - } - - /// Return the area enclosed by the `Subpath` always considering it as a closed subpath. It will always give a positive value. - /// - /// If the area is less than `error`, it will return zero. - /// Because the calculation of area for self-intersecting path requires finding the intersections, the following parameters are used: - /// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point. - /// - `minimum_separation` - the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order. - /// - /// If the comparison condition is not satisfied, the function takes the larger `t`-value of the two - /// - /// **NOTE**: if an intersection were to occur within an `error` distance away from an anchor point, the algorithm will filter that intersection out. - pub fn area(&self, error: Option, minimum_separation: Option) -> f64 { - let all_intersections = self.all_self_intersections(error, minimum_separation); - let mut current_sign: f64 = 1.; - - let area: f64 = self - .iter_closed() - .enumerate() - .map(|(index, bezier)| { - let (f_x, f_y) = bezier.parametric_polynomial(); - let (f_x, mut f_y) = (f_x.as_size::<7>().unwrap(), f_y.as_size::<7>().unwrap()); - f_y.derivative_mut(); - f_y *= &f_x; - f_y.antiderivative_mut(); - - let mut curve_sum = -current_sign * f_y.eval(0.); - for (_, t) in all_intersections.iter().filter(|(i, _)| *i == index) { - curve_sum += 2. * current_sign * f_y.eval(*t); - current_sign *= -1.; - } - curve_sum += current_sign * f_y.eval(1.); - curve_sum - }) - .sum(); - - if area.abs() < error.unwrap_or(MAX_ABSOLUTE_DIFFERENCE) { - return 0.; - } - - area.abs() - } - - /// Return the area centroid, together with the area, of the `Subpath` always considering it as a closed subpath. The area will always be a positive value. - /// - /// The area centroid is the center of mass for the area of a solid shape's interior. - /// An infinitely flat material forming the subpath's closed shape would balance at this point. - /// - /// It will return `None` if no manipulator is present. If the area is less than `error`, it will return `Some((DVec2::NAN, 0.))`. - /// - /// Because the calculation of area and centroid for self-intersecting path requires finding the intersections, the following parameters are used: - /// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point. - /// - `minimum_separation` - the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order. - /// - /// If the comparison condition is not satisfied, the function takes the larger `t`-value of the two. - /// - /// **NOTE**: if an intersection were to occur within an `error` distance away from an anchor point, the algorithm will filter that intersection out. - pub fn area_centroid_and_area(&self, error: Option, minimum_separation: Option) -> Option<(DVec2, f64)> { - let all_intersections = self.all_self_intersections(error, minimum_separation); - let mut current_sign: f64 = 1.; - - let (x_sum, y_sum, area) = self - .iter_closed() - .enumerate() - .map(|(index, bezier)| { - let (f_x, f_y) = bezier.parametric_polynomial(); - let (f_x, f_y) = (f_x.as_size::<10>().unwrap(), f_y.as_size::<10>().unwrap()); - let f_y_prime = f_y.derivative(); - let f_x_prime = f_x.derivative(); - let f_xy = &f_x * &f_y; - - let mut x_part = &f_xy * &f_x_prime; - let mut y_part = &f_xy * &f_y_prime; - let mut area_part = &f_x * &f_y_prime; - x_part.antiderivative_mut(); - y_part.antiderivative_mut(); - area_part.antiderivative_mut(); - - let mut curve_sum_x = -current_sign * x_part.eval(0.); - let mut curve_sum_y = -current_sign * y_part.eval(0.); - let mut curve_sum_area = -current_sign * area_part.eval(0.); - for (_, t) in all_intersections.iter().filter(|(i, _)| *i == index) { - curve_sum_x += 2. * current_sign * x_part.eval(*t); - curve_sum_y += 2. * current_sign * y_part.eval(*t); - curve_sum_area += 2. * current_sign * area_part.eval(*t); - current_sign *= -1.; - } - curve_sum_x += current_sign * x_part.eval(1.); - curve_sum_y += current_sign * y_part.eval(1.); - curve_sum_area += current_sign * area_part.eval(1.); - - (-curve_sum_x, curve_sum_y, curve_sum_area) - }) - .reduce(|(x1, y1, area1), (x2, y2, area2)| (x1 + x2, y1 + y2, area1 + area2))?; - - if area.abs() < error.unwrap_or(MAX_ABSOLUTE_DIFFERENCE) { - return Some((DVec2::NAN, 0.)); - } - - Some((DVec2::new(x_sum / area, y_sum / area), area.abs())) - } - - /// Attempts to return the area centroid of the `Subpath` always considering it as a closed subpath. Falls back to length centroid if the area is zero. - /// - /// The area centroid is the center of mass for the area of a solid shape's interior. - /// An infinitely flat material forming the subpath's closed shape would balance at this point. - /// - /// It will return `None` if no manipulator is present. - /// Because the calculation of centroid for self-intersecting path requires finding the intersections, the following parameters are used: - /// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point. - /// - `minimum_separation` - the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order. - /// - `tolerance` - Tolerance used to approximate the curve if it falls back to length centroid. - /// - /// If the comparison condition is not satisfied, the function takes the larger `t`-value of the two - /// - /// **NOTE**: if an intersection were to occur within an `error` distance away from an anchor point, the algorithm will filter that intersection out. - /// - pub fn area_centroid(&self, error: Option, minimum_separation: Option, tolerance: Option) -> Option { - let (centroid, area) = self.area_centroid_and_area(error, minimum_separation)?; - - if area != 0. { - Some(centroid) - } else { - self.length_centroid_and_length(tolerance, true).map(|(centroid, _)| centroid) - } - } - - /// Converts from a subpath (composed of multiple segments) to a point along a certain segment represented. - /// The returned tuple represents the segment index and the `t` value along that segment. - /// Both the input global `t` value and the output `t` value are in euclidean space, meaning there is a constant rate of change along the arc length. - pub fn global_euclidean_to_local_euclidean(&self, global_t: f64, lengths: &[f64], total_length: f64) -> (usize, f64) { - let mut accumulator = 0.; - for (index, length) in lengths.iter().enumerate() { - let length_ratio = length / total_length; - if (index == 0 || accumulator <= global_t) && global_t <= accumulator + length_ratio { - return (index, ((global_t - accumulator) / length_ratio).clamp(0., 1.)); - } - accumulator += length_ratio; - } - (self.len() - 2, 1.) - } - - /// Convert a [SubpathTValue] to a parametric `(segment_index, t)` tuple. - /// - Asserts that `t` values contained within the `SubpathTValue` argument lie in the range [0, 1]. - /// - If the argument is a variant containing a `segment_index`, asserts that the index references a valid segment on the curve. - pub(crate) fn t_value_to_parametric(&self, t: SubpathTValue) -> (usize, f64) { - assert!(self.len_segments() >= 1); - - match t { - SubpathTValue::Parametric { segment_index, t } => { - assert!((0.0..=1.).contains(&t)); - assert!((0..self.len_segments()).contains(&segment_index)); - (segment_index, t) - } - SubpathTValue::GlobalParametric(global_t) => { - assert!((0.0..=1.).contains(&global_t)); - - if global_t == 1. { - return (self.len_segments() - 1, 1.); - } - - let scaled_t = global_t * self.len_segments() as f64; - let segment_index = scaled_t.floor() as usize; - let t = scaled_t - segment_index as f64; - - (segment_index, t) - } - SubpathTValue::Euclidean { segment_index, t } => { - assert!((0.0..=1.).contains(&t)); - assert!((0..self.len_segments()).contains(&segment_index)); - (segment_index, self.get_segment(segment_index).unwrap().euclidean_to_parametric(t, DEFAULT_EUCLIDEAN_ERROR_BOUND)) - } - SubpathTValue::GlobalEuclidean(t) => { - let lengths = self.iter().map(|bezier| bezier.length(None)).collect::>(); - let total_length: f64 = lengths.iter().sum(); - let (segment_index, segment_t_euclidean) = self.global_euclidean_to_local_euclidean(t, lengths.as_slice(), total_length); - let segment_t_parametric = self.get_segment(segment_index).unwrap().euclidean_to_parametric(segment_t_euclidean, DEFAULT_EUCLIDEAN_ERROR_BOUND); - (segment_index, segment_t_parametric) - } - SubpathTValue::EuclideanWithinError { segment_index, t, error } => { - assert!((0.0..=1.).contains(&t)); - assert!((0..self.len_segments()).contains(&segment_index)); - (segment_index, self.get_segment(segment_index).unwrap().euclidean_to_parametric(t, error)) - } - SubpathTValue::GlobalEuclideanWithinError { t, error } => { - let lengths = self.iter().map(|bezier| bezier.length(None)).collect::>(); - let total_length: f64 = lengths.iter().sum(); - let (segment_index, segment_t) = self.global_euclidean_to_local_euclidean(t, lengths.as_slice(), total_length); - (segment_index, self.get_segment(segment_index).unwrap().euclidean_to_parametric(segment_t, error)) - } - } - } - - /// Returns the segment index and `t` value that corresponds to the closest point on the curve to the provided point. - /// - pub fn project(&self, point: DVec2) -> Option<(usize, f64)> { - if self.is_empty() { - return None; - } - - // TODO: Optimization opportunity: Filter out segments which are *definitely* not the closest to the given point - let (index, (_, project_t)) = self - .iter() - .map(|bezier| { - let project_t = bezier.project(point); - (bezier.evaluate(TValue::Parametric(project_t)).distance(point), project_t) - }) - .enumerate() - .min_by(|(_, (distance1, _)), (_, (distance2, _))| distance1.total_cmp(distance2)) - .unwrap_or((0, (0., 0.))); // If the Subpath contains only a single manipulator group, returns (0, 0.) - - Some((index, project_t)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::consts::MAX_ABSOLUTE_DIFFERENCE; - use crate::utils::f64_compare; - - #[test] - fn length_quadratic() { - let start = DVec2::new(20., 30.); - let middle = DVec2::new(80., 90.); - let end = DVec2::new(60., 45.); - let handle1 = DVec2::new(75., 85.); - let handle2 = DVec2::new(40., 30.); - let handle3 = DVec2::new(10., 10.); - - let bezier1 = Bezier::from_quadratic_dvec2(start, handle1, middle); - let bezier2 = Bezier::from_quadratic_dvec2(middle, handle2, end); - let bezier3 = Bezier::from_quadratic_dvec2(end, handle3, start); - - let mut subpath = Subpath::new( - vec![ - ManipulatorGroup { - anchor: start, - in_handle: None, - out_handle: Some(handle1), - id: EmptyId, - }, - ManipulatorGroup { - anchor: middle, - in_handle: None, - out_handle: Some(handle2), - id: EmptyId, - }, - ManipulatorGroup { - anchor: end, - in_handle: None, - out_handle: Some(handle3), - id: EmptyId, - }, - ], - false, - ); - assert_eq!(subpath.length(None), bezier1.length(None) + bezier2.length(None)); - - subpath.closed = true; - assert_eq!(subpath.length(None), bezier1.length(None) + bezier2.length(None) + bezier3.length(None)); - } - - #[test] - fn length_mixed() { - let start = DVec2::new(20., 30.); - let middle = DVec2::new(70., 70.); - let end = DVec2::new(60., 45.); - let handle1 = DVec2::new(75., 85.); - let handle2 = DVec2::new(40., 30.); - let handle3 = DVec2::new(10., 10.); - - let linear_bezier = Bezier::from_linear_dvec2(start, middle); - let quadratic_bezier = Bezier::from_quadratic_dvec2(middle, handle1, end); - let cubic_bezier = Bezier::from_cubic_dvec2(end, handle2, handle3, start); - - let mut subpath = Subpath::new( - vec![ - ManipulatorGroup { - anchor: start, - in_handle: Some(handle3), - out_handle: None, - id: EmptyId, - }, - ManipulatorGroup { - anchor: middle, - in_handle: None, - out_handle: Some(handle1), - id: EmptyId, - }, - ManipulatorGroup { - anchor: end, - in_handle: None, - out_handle: Some(handle2), - id: EmptyId, - }, - ], - false, - ); - assert_eq!(subpath.length(None), linear_bezier.length(None) + quadratic_bezier.length(None)); - - subpath.closed = true; - assert_eq!(subpath.length(None), linear_bezier.length(None) + quadratic_bezier.length(None) + cubic_bezier.length(None)); - } - - #[test] - fn length_centroid() { - let start = DVec2::new(0., 0.); - let end = DVec2::new(1., 1.); - let handle = DVec2::new(0., 1.); - - let mut subpath = Subpath::new( - vec![ - ManipulatorGroup { - anchor: start, - in_handle: None, - out_handle: Some(handle), - id: EmptyId, - }, - ManipulatorGroup { - anchor: end, - in_handle: None, - out_handle: None, - id: EmptyId, - }, - ], - false, - ); - - let expected_centroid = DVec2::new(0.4153039799983826, 0.5846960200016174); - let epsilon = 0.00001; - - assert!(subpath.length_centroid_and_length(None, true).unwrap().0.abs_diff_eq(expected_centroid, epsilon)); - - subpath.closed = true; - assert!(subpath.length_centroid_and_length(None, true).unwrap().0.abs_diff_eq(expected_centroid, epsilon)); - } - - #[test] - fn area() { - let start = DVec2::new(0., 0.); - let end = DVec2::new(1., 1.); - let handle = DVec2::new(0., 1.); - - let mut subpath = Subpath::new( - vec![ - ManipulatorGroup { - anchor: start, - in_handle: None, - out_handle: Some(handle), - id: EmptyId, - }, - ManipulatorGroup { - anchor: end, - in_handle: None, - out_handle: None, - id: EmptyId, - }, - ], - false, - ); - - let expected_area = 1. / 3.; - let epsilon = 0.00001; - - assert!((subpath.area(Some(0.001), Some(0.001)) - expected_area).abs() < epsilon); - - subpath.closed = true; - assert!((subpath.area(Some(0.001), Some(0.001)) - expected_area).abs() < epsilon); - } - - #[test] - fn area_centroid() { - let start = DVec2::new(0., 0.); - let end = DVec2::new(1., 1.); - let handle = DVec2::new(0., 1.); - - let mut subpath = Subpath::new( - vec![ - ManipulatorGroup { - anchor: start, - in_handle: None, - out_handle: Some(handle), - id: EmptyId, - }, - ManipulatorGroup { - anchor: end, - in_handle: None, - out_handle: None, - id: EmptyId, - }, - ], - false, - ); - - let expected_centroid = DVec2::new(0.4, 0.6); - let epsilon = 0.00001; - - assert!(subpath.area_centroid(Some(0.001), Some(0.001), None).unwrap().abs_diff_eq(expected_centroid, epsilon)); - - subpath.closed = true; - assert!(subpath.area_centroid(Some(0.001), Some(0.001), None).unwrap().abs_diff_eq(expected_centroid, epsilon)); - } - - #[test] - fn t_value_to_parametric_global_parametric_open_subpath() { - let mock_manipulator_group = ManipulatorGroup { - anchor: DVec2::new(0., 0.), - in_handle: None, - out_handle: None, - id: EmptyId, - }; - let open_subpath = Subpath { - manipulator_groups: vec![mock_manipulator_group; 5], - closed: false, - }; - - let (segment_index, t) = open_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(0.7)); - assert_eq!(segment_index, 2); - assert!(f64_compare(t, 0.8, MAX_ABSOLUTE_DIFFERENCE)); - - // The start and end points of an open subpath are NOT equivalent - assert_eq!(open_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(0.)), (0, 0.)); - assert_eq!(open_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(1.)), (3, 1.)); - } - - #[test] - fn t_value_to_parametric_global_parametric_closed_subpath() { - let mock_manipulator_group = ManipulatorGroup { - anchor: DVec2::new(0., 0.), - in_handle: None, - out_handle: None, - id: EmptyId, - }; - let closed_subpath = Subpath { - manipulator_groups: vec![mock_manipulator_group; 5], - closed: true, - }; - - let (segment_index, t) = closed_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(0.7)); - assert_eq!(segment_index, 3); - assert!(f64_compare(t, 0.5, MAX_ABSOLUTE_DIFFERENCE)); - - // The start and end points of a closed subpath are equivalent - assert_eq!(closed_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(0.)), (0, 0.)); - assert_eq!(closed_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(1.)), (4, 1.)); - } - - #[test] - fn exact_start_end() { - let start = DVec2::new(20., 30.); - let end = DVec2::new(60., 45.); - let handle = DVec2::new(75., 85.); - - let subpath: Subpath = Subpath::from_bezier(&Bezier::from_quadratic_dvec2(start, handle, end)); - - assert_eq!(subpath.evaluate(SubpathTValue::GlobalEuclidean(0.)), start); - assert_eq!(subpath.evaluate(SubpathTValue::GlobalEuclidean(1.)), end); - } -} diff --git a/libraries/bezier-rs/src/subpath/manipulators.rs b/libraries/bezier-rs/src/subpath/manipulators.rs deleted file mode 100644 index 4faf28d26a..0000000000 --- a/libraries/bezier-rs/src/subpath/manipulators.rs +++ /dev/null @@ -1,231 +0,0 @@ -use super::*; -use crate::consts::MAX_ABSOLUTE_DIFFERENCE; -use crate::utils::f64_compare; -use crate::{SubpathTValue, TValue}; - -impl Subpath { - /// Get whether the subpath is closed. - pub fn closed(&self) -> bool { - self.closed - } - - /// Set whether the subpath is closed. - pub fn set_closed(&mut self, new_closed: bool) { - self.closed = new_closed; - } - - /// Access a [ManipulatorGroup] from a PointId. - pub fn manipulator_from_id(&self, id: PointId) -> Option<&ManipulatorGroup> { - self.manipulator_groups.iter().find(|manipulator_group| manipulator_group.id == id) - } - - /// Access a mutable [ManipulatorGroup] from a PointId. - pub fn manipulator_mut_from_id(&mut self, id: PointId) -> Option<&mut ManipulatorGroup> { - self.manipulator_groups.iter_mut().find(|manipulator_group| manipulator_group.id == id) - } - - /// Access the index of a [ManipulatorGroup] from a PointId. - pub fn manipulator_index_from_id(&self, id: PointId) -> Option { - self.manipulator_groups.iter().position(|manipulator_group| manipulator_group.id == id) - } - - /// Insert a manipulator group at an index. - pub fn insert_manipulator_group(&mut self, index: usize, group: ManipulatorGroup) { - assert!(group.is_finite(), "Inserting non finite manipulator group"); - self.manipulator_groups.insert(index, group) - } - - /// Push a manipulator group to the end. - pub fn push_manipulator_group(&mut self, group: ManipulatorGroup) { - assert!(group.is_finite(), "Pushing non finite manipulator group"); - self.manipulator_groups.push(group) - } - - /// Get a mutable reference to the last manipulator - pub fn last_manipulator_group_mut(&mut self) -> Option<&mut ManipulatorGroup> { - self.manipulator_groups.last_mut() - } - - /// Remove a manipulator group at an index. - pub fn remove_manipulator_group(&mut self, index: usize) -> ManipulatorGroup { - self.manipulator_groups.remove(index) - } - - /// Inserts a `ManipulatorGroup` at a certain point along the subpath based on the parametric `t`-value provided. - /// Expects `t` to be within the inclusive range `[0, 1]`. - pub fn insert(&mut self, t: SubpathTValue) { - let (segment_index, t) = self.t_value_to_parametric(t); - - if f64_compare(t, 0., MAX_ABSOLUTE_DIFFERENCE) || f64_compare(t, 1., MAX_ABSOLUTE_DIFFERENCE) { - return; - } - - // The only case where `curve` would be `None` is if the provided argument was 1 - // But the above if case would catch that, since `target_curve_t` would be 0. - let curve = self.iter().nth(segment_index).unwrap(); - - let [first, second] = curve.split(TValue::Parametric(t)); - let new_group = ManipulatorGroup { - anchor: first.end(), - in_handle: first.handle_end(), - out_handle: second.handle_start(), - id: PointId::new(), - }; - let number_of_groups = self.manipulator_groups.len() + 1; - self.manipulator_groups.insert((segment_index) + 1, new_group); - self.manipulator_groups[segment_index % number_of_groups].out_handle = first.handle_start(); - self.manipulator_groups[(segment_index + 2) % number_of_groups].in_handle = second.handle_end(); - } - - /// Append a [Bezier] to the end of a subpath from a vector of [Bezier]. - /// The `append_type` parameter determines how the function behaves when the subpath's last anchor is not equal to the Bezier's start point. - /// - `IgnoreStart`: drops the bezier's start point in favor of the subpath's last anchor - /// - `SmoothJoin(f64)`: joins the subpath's endpoint with the bezier's start with a another Bezier segment that is continuous up to the second derivative - /// if the difference between the subpath's end point and Bezier's start point exceeds the wrapped integer value. - /// - /// This function assumes that the position of the [Bezier]'s starting point is equal to that of the Subpath's last manipulator group. - pub fn append_bezier(&mut self, bezier: &Bezier, append_type: AppendType) { - if self.manipulator_groups.is_empty() { - self.manipulator_groups = vec![ManipulatorGroup { - anchor: bezier.start(), - in_handle: None, - out_handle: None, - id: PointId::new(), - }]; - } - let mut last_index = self.manipulator_groups.len() - 1; - let last_anchor = self.manipulator_groups[last_index].anchor; - - if let AppendType::SmoothJoin(max_absolute_difference) = append_type { - // If the provided Bezier does not start at a location similar to the end of the Subpath, - // add an additional manipulator group to represent a smooth join with a new bezier in between - if !last_anchor.abs_diff_eq(bezier.start(), max_absolute_difference) { - let last_bezier = if self.manipulator_groups.len() > 1 { - self.manipulator_groups[last_index - 1].to_bezier(&self.manipulator_groups[last_index]) - } else { - Bezier::from_linear_dvec2(last_anchor, last_anchor) - }; - let join_bezier = last_bezier.join(bezier); - self.append_bezier(&join_bezier, AppendType::IgnoreStart); - last_index = self.manipulator_groups.len() - 1; - } - } - self.manipulator_groups[last_index].out_handle = bezier.handle_start(); - self.manipulator_groups.push(ManipulatorGroup { - anchor: bezier.end(), - in_handle: bezier.handle_end(), - out_handle: None, - id: PointId::new(), - }); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::utils::SubpathTValue; - use glam::DVec2; - - fn set_up_open_subpath() -> Subpath { - let start = DVec2::new(20., 30.); - let middle1 = DVec2::new(80., 90.); - let middle2 = DVec2::new(100., 100.); - let end = DVec2::new(60., 45.); - - let handle1 = DVec2::new(75., 85.); - let handle2 = DVec2::new(40., 30.); - let handle3 = DVec2::new(10., 10.); - - Subpath::new( - vec![ - ManipulatorGroup { - anchor: start, - in_handle: None, - out_handle: Some(handle1), - id: EmptyId, - }, - ManipulatorGroup { - anchor: middle1, - in_handle: None, - out_handle: Some(handle2), - id: EmptyId, - }, - ManipulatorGroup { - anchor: middle2, - in_handle: None, - out_handle: None, - id: EmptyId, - }, - ManipulatorGroup { - anchor: end, - in_handle: None, - out_handle: Some(handle3), - id: EmptyId, - }, - ], - false, - ) - } - - fn set_up_closed_subpath() -> Subpath { - let mut subpath = set_up_open_subpath(); - subpath.closed = true; - subpath - } - - #[test] - fn insert_in_first_segment_of_open_subpath() { - let mut subpath = set_up_open_subpath(); - let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); - let split_pair = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 3.) % 1.)); - subpath.insert(SubpathTValue::GlobalParametric(0.2)); - assert_eq!(subpath.manipulator_groups[1].anchor, location); - assert_eq!(split_pair[0], subpath.iter().next().unwrap()); - assert_eq!(split_pair[1], subpath.iter().nth(1).unwrap()); - } - - #[test] - fn insert_in_last_segment_of_open_subpath() { - let mut subpath = set_up_open_subpath(); - let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.9)); - let split_pair = subpath.iter().nth(2).unwrap().split(TValue::Parametric((0.9 * 3.) % 1.)); - subpath.insert(SubpathTValue::GlobalParametric(0.9)); - assert_eq!(subpath.manipulator_groups[3].anchor, location); - assert_eq!(split_pair[0], subpath.iter().nth(2).unwrap()); - assert_eq!(split_pair[1], subpath.iter().nth(3).unwrap()); - } - - #[test] - fn insert_at_existing_manipulator_group_of_open_subpath() { - // This will do nothing to the subpath - let mut subpath = set_up_open_subpath(); - let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.75)); - subpath.insert(SubpathTValue::GlobalParametric(0.75)); - assert_eq!(subpath.manipulator_groups[3].anchor, location); - assert_eq!(subpath.manipulator_groups.len(), 5); - assert_eq!(subpath.len_segments(), 4); - } - - #[test] - fn insert_at_last_segment_of_closed_subpath() { - let mut subpath = set_up_closed_subpath(); - let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.9)); - let split_pair = subpath.iter().nth(3).unwrap().split(TValue::Parametric((0.9 * 4.) % 1.)); - subpath.insert(SubpathTValue::GlobalParametric(0.9)); - assert_eq!(subpath.manipulator_groups[4].anchor, location); - assert_eq!(split_pair[0], subpath.iter().nth(3).unwrap()); - assert_eq!(split_pair[1], subpath.iter().nth(4).unwrap()); - assert!(subpath.closed); - } - - #[test] - fn insert_at_last_manipulator_group_of_closed_subpath() { - // This will do nothing to the subpath - let mut subpath = set_up_closed_subpath(); - let location = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); - subpath.insert(SubpathTValue::GlobalParametric(1.)); - assert_eq!(subpath.manipulator_groups[0].anchor, location); - assert_eq!(subpath.manipulator_groups.len(), 4); - assert!(subpath.closed); - } -} diff --git a/libraries/bezier-rs/src/subpath/mod.rs b/libraries/bezier-rs/src/subpath/mod.rs deleted file mode 100644 index c349e01e7c..0000000000 --- a/libraries/bezier-rs/src/subpath/mod.rs +++ /dev/null @@ -1,75 +0,0 @@ -mod core; -mod lookup; -mod manipulators; -mod solvers; -mod structs; -mod transform; - -use crate::Bezier; -pub use core::*; -use std::fmt::{Debug, Formatter, Result}; -use std::ops::{Index, IndexMut}; -pub use structs::*; - -/// Structure used to represent a path composed of [Bezier] curves. -#[derive(Clone, PartialEq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Subpath { - manipulator_groups: Vec>, - pub closed: bool, -} - -#[cfg(feature = "dyn-any")] -unsafe impl dyn_any::StaticType for Subpath { - type Static = Subpath; -} - -/// Iteration structure for iterating across each curve of a `Subpath`, using an intermediate `Bezier` representation. -pub struct SubpathIter<'a, PointId: crate::Identifier> { - index: usize, - subpath: &'a Subpath, - is_always_closed: bool, -} - -impl Index for Subpath { - type Output = ManipulatorGroup; - - fn index(&self, index: usize) -> &Self::Output { - assert!(index < self.len(), "Index out of bounds in trait Index of SubPath."); - &self.manipulator_groups[index] - } -} - -impl IndexMut for Subpath { - fn index_mut(&mut self, index: usize) -> &mut Self::Output { - assert!(index < self.len(), "Index out of bounds in trait IndexMut of SubPath."); - &mut self.manipulator_groups[index] - } -} - -impl Iterator for SubpathIter<'_, PointId> { - type Item = Bezier; - - // Returns the Bezier representation of each `Subpath` segment, defined between a pair of adjacent manipulator points. - fn next(&mut self) -> Option { - if self.subpath.is_empty() { - return None; - } - let closed = if self.is_always_closed { true } else { self.subpath.closed }; - let len = self.subpath.len() - 1 + if closed { 1 } else { 0 }; - if self.index >= len { - return None; - } - let start_index = self.index; - let end_index = (self.index + 1) % self.subpath.len(); - self.index += 1; - - Some(self.subpath[start_index].to_bezier(&self.subpath[end_index])) - } -} - -impl Debug for Subpath { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - f.debug_struct("Subpath").field("closed", &self.closed).field("manipulator_groups", &self.manipulator_groups).finish() - } -} diff --git a/libraries/bezier-rs/src/subpath/solvers.rs b/libraries/bezier-rs/src/subpath/solvers.rs deleted file mode 100644 index d3558fc22d..0000000000 --- a/libraries/bezier-rs/src/subpath/solvers.rs +++ /dev/null @@ -1,1067 +0,0 @@ -use super::*; -use crate::TValue; -use crate::consts::MAX_ABSOLUTE_DIFFERENCE; -use crate::utils::{SubpathTValue, compute_circular_subpath_details, is_rectangle_inside_other, line_intersection}; -use glam::{DAffine2, DMat2, DVec2}; -use std::f64::consts::PI; - -impl Subpath { - /// Calculate the point on the subpath based on the parametric `t`-value provided. - /// Expects `t` to be within the inclusive range `[0, 1]`. - /// - pub fn evaluate(&self, t: SubpathTValue) -> DVec2 { - let (segment_index, t) = self.t_value_to_parametric(t); - self.get_segment(segment_index).unwrap().evaluate(TValue::Parametric(t)) - } - - /// Calculates the intersection points the subpath has with a given curve and returns a list of `(usize, f64)` tuples, - /// where the `usize` represents the index of the curve in the subpath, and the `f64` represents the `t`-value local to - /// that curve where the intersection occurred. - /// Expects the following: - /// - `other`: a [Bezier] curve to check intersections against - /// - `error`: an optional f64 value to provide an error bound - /// - `minimum_separation`: the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order. - /// - /// If the comparison condition is not satisfied, the function takes the larger `t`-value of the two. - /// - /// - /// - /// - /// - pub fn intersections(&self, other: &Bezier, error: Option, minimum_separation: Option) -> Vec<(usize, f64)> { - self.iter() - .enumerate() - .flat_map(|(index, bezier)| bezier.intersections(other, error, minimum_separation).into_iter().map(|t| (index, t)).collect::>()) - .collect() - } - - /// Calculates the intersection points the subpath has with another given subpath and returns a list of global parametric `t`-values. - /// This function expects the following: - /// - other: a [Bezier] curve to check intersections against - /// - error: an optional f64 value to provide an error bound - pub fn subpath_intersections(&self, other: &Subpath, error: Option, minimum_separation: Option) -> Vec<(usize, f64)> { - let mut intersection_t_values: Vec<(usize, f64)> = other.iter().flat_map(|bezier| self.intersections(&bezier, error, minimum_separation)).collect(); - intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap()); - intersection_t_values - } - - /// Returns how many times a given ray intersects with this subpath. (`ray_direction` does not need to be normalized.) - /// If this needs to be called frequently with a ray of the same rotation angle, consider instead using [`ray_test_crossings_count_prerotated`]. - pub fn ray_test_crossings_count(&self, ray_start: DVec2, ray_direction: DVec2) -> usize { - self.iter().map(|bezier| bezier.ray_test_crossings(ray_start, ray_direction).count()).sum() - } - - /// Returns how many times a given ray intersects with this subpath. (`ray_direction` does not need to be normalized.) - /// This version of the function is for better performance when calling it frequently without needing to change the rotation between each call. - /// If that isn't important, use [`ray_test_crossings_count`] which provides an easier interface by taking a ray direction vector. - /// Instead, this version requires a rotation matrix for the ray's rotation and a prerotated version of this subpath that has had its rotation applied. - pub fn ray_test_crossings_count_prerotated(&self, ray_start: DVec2, rotation_matrix: DMat2, rotated_subpath: &Self) -> usize { - self.iter() - .zip(rotated_subpath.iter()) - .map(|(bezier, rotated_bezier)| bezier.ray_test_crossings_prerotated(ray_start, rotation_matrix, rotated_bezier).count()) - .sum() - } - - /// Returns true if the given point is inside this subpath. Open paths are NOT automatically closed so you'll need to call `set_closed(true)` before calling this. - /// Self-intersecting subpaths use the `evenodd` fill rule for checking in/outside-ness: . - /// If this needs to be called frequently, consider instead using [`point_inside_prerotated`] and moving this function's setup code into your own logic before the repeated call. - pub fn point_inside(&self, point: DVec2) -> bool { - // The directions use prime numbers to reduce the likelihood of running across two anchor points simultaneously - const SIN_13DEG: f64 = 0.22495105434; - const COS_13DEG: f64 = 0.97437006478; - const DIRECTION1: DVec2 = DVec2::new(SIN_13DEG, COS_13DEG); - const DIRECTION2: DVec2 = DVec2::new(-COS_13DEG, -SIN_13DEG); - - // We (inefficiently) check for odd crossings in two directions and make sure they agree to reduce how often anchor points cause a double-increment - let test1 = self.ray_test_crossings_count(point, DIRECTION1) % 2 == 1; - let test2 = self.ray_test_crossings_count(point, DIRECTION2) % 2 == 1; - - test1 && test2 - } - - /// Returns true if the given point is inside this subpath. Open paths are NOT automatically closed so you'll need to call `set_closed(true)` before calling this. - /// Self-intersecting subpaths use the `evenodd` fill rule for checking in/outside-ness: . - /// This version of the function is for better performance when calling it frequently because it lets the caller precompute the rotations once instead of every call. - /// If that isn't important, use [`point_inside`] which provides an easier interface. - /// Instead, this version requires a pair of rotation matrices for the ray's rotation and a pair of prerotated versions of this subpath. - /// They should face in different directions that are unlikely to align in the real world. Consider using the following rotations: - /// ```rs - /// const SIN_13DEG: f64 = 0.22495105434; - /// const COS_13DEG: f64 = 0.97437006478; - /// const DIRECTION1: DVec2 = DVec2::new(SIN_13DEG, COS_13DEG); - /// const DIRECTION2: DVec2 = DVec2::new(-COS_13DEG, -SIN_13DEG); - /// ``` - pub fn point_inside_prerotated(&self, point: DVec2, rotation_matrix1: DMat2, rotation_matrix2: DMat2, rotated_subpath1: &Self, rotated_subpath2: &Self) -> bool { - // We (inefficiently) check for odd crossings in two directions and make sure they agree to reduce how often anchor points cause a double-increment - let test1 = self.ray_test_crossings_count_prerotated(point, rotation_matrix1, rotated_subpath1) % 2 == 1; - let test2 = self.ray_test_crossings_count_prerotated(point, rotation_matrix2, rotated_subpath2) % 2 == 1; - - test1 && test2 - } - - /// Computes the winding number contribution of the subpath. - pub fn winding_order(&self, point: DVec2) -> i32 { - self.iter().map(|segment| segment.winding(point)).sum() - } - - /// Returns a list of `t` values that correspond to the self intersection points of the subpath. For each intersection point, the returned `t` value is the smaller of the two that correspond to the point. - /// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point. - /// - `minimum_separation`: the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order. - /// - /// If the comparison condition is not satisfied, the function takes the larger `t`-value of the two - /// - /// **NOTE**: if an intersection were to occur within an `error` distance away from an anchor point, the algorithm will filter that intersection out. - /// - pub fn self_intersections(&self, error: Option, minimum_separation: Option) -> Vec<(usize, f64)> { - let mut intersections_vec = Vec::new(); - let err = error.unwrap_or(MAX_ABSOLUTE_DIFFERENCE); - // TODO: optimization opportunity - this for-loop currently compares all intersections with all curve-segments in the subpath collection - self.iter().enumerate().for_each(|(i, other)| { - intersections_vec.extend(other.self_intersections(error, minimum_separation).iter().map(|value| (i, value[0]))); - self.iter().enumerate().skip(i + 1).for_each(|(j, curve)| { - intersections_vec.extend( - curve - .intersections(&other, error, minimum_separation) - .iter() - .filter(|&value| value > &err && (1. - value) > err) - .map(|value| (j, *value)), - ); - }); - }); - intersections_vec - } - - /// Returns a list of `t` values that correspond to all the self intersection points of the subpath always considering it as a closed subpath. The index and `t` value of both will be returned that corresponds to a point. - /// The points will be sorted based on their index and `t` repsectively. - /// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point. - /// - `minimum_separation`: the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order. - /// - /// If the comparison condition is not satisfied, the function takes the larger `t`-value of the two - /// - /// **NOTE**: if an intersection were to occur within an `error` distance away from an anchor point, the algorithm will filter that intersection out. - pub fn all_self_intersections(&self, error: Option, minimum_separation: Option) -> Vec<(usize, f64)> { - let mut intersections_vec = Vec::new(); - let err = error.unwrap_or(MAX_ABSOLUTE_DIFFERENCE); - let num_curves = self.len(); - // TODO: optimization opportunity - this for-loop currently compares all intersections with all curve-segments in the subpath collection - self.iter_closed().enumerate().for_each(|(i, other)| { - intersections_vec.extend(other.self_intersections(error, minimum_separation).iter().flat_map(|value| [(i, value[0]), (i, value[1])])); - self.iter_closed().enumerate().skip(i + 1).for_each(|(j, curve)| { - intersections_vec.extend( - curve - .all_intersections(&other, error, minimum_separation) - .iter() - .filter(|&value| (j != i + 1 || value[0] > err || (1. - value[1]) > err) && (j != num_curves - 1 || i != 0 || value[1] > err || (1. - value[0]) > err)) - .flat_map(|value| [(j, value[0]), (i, value[1])]), - ); - }); - }); - - intersections_vec.sort_by(|a, b| a.partial_cmp(b).unwrap()); - - intersections_vec - } - - /// Calculates the intersection points the subpath has with a given rectangle and returns a list of `(usize, f64)` tuples, - /// where the `usize` represents the index of the curve in the subpath, and the `f64` represents the `t`-value local to - /// that curve where the intersection occurred. - /// Expects the following: - /// - `corner1`: any corner of the axis-aligned box to intersect with - /// - `corner2`: the corner opposite to `corner1` - /// - `error`: an optional f64 value to provide an error bound - /// - `minimum_separation`: the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order. - /// - /// If the comparison condition is not satisfied, the function takes the larger `t`-value of the two. - /// - pub fn rectangle_intersections(&self, corner1: DVec2, corner2: DVec2, error: Option, minimum_separation: Option) -> Vec<(usize, f64)> { - [ - Bezier::from_linear_coordinates(corner1.x, corner1.y, corner2.x, corner1.y), - Bezier::from_linear_coordinates(corner2.x, corner1.y, corner2.x, corner2.y), - Bezier::from_linear_coordinates(corner2.x, corner2.y, corner1.x, corner2.y), - Bezier::from_linear_coordinates(corner1.x, corner2.y, corner1.x, corner1.y), - ] - .iter() - .flat_map(|bezier| self.intersections(bezier, error, minimum_separation)) - .collect() - } - - /// Checks if any intersections exist between this subpath and the four edges of the rectangle defined by the top-left `corner1` and bottom-right `corner2`. - /// This is faster than calling [`rectangle_intersections`]`.len()` because it short-circuits as soon as an intersection is found. - pub fn rectangle_intersections_exist(&self, corner1: DVec2, corner2: DVec2) -> bool { - let rotate_by_90deg = |point| DMat2::from_angle(std::f64::consts::FRAC_PI_2) * point; - - for bezier in self.iter() { - // Check that the two bounding boxes don't intersect, since we can avoid doing intersection's cubic root finding in that case - let [bezier_corner1, bezier_corner2] = bezier.bounding_box_of_anchors_and_handles(); - if !(((corner1.x < bezier_corner1.x) && (bezier_corner1.x < corner2.x) || (corner1.x < bezier_corner2.x) && (bezier_corner2.x < corner2.x)) - && corner1.y < bezier_corner2.y - && corner2.y > bezier_corner1.y - || ((corner1.y < bezier_corner1.y) && (bezier_corner1.y < corner2.y) || (corner1.y < bezier_corner2.y) && (bezier_corner2.y < corner2.y)) - && corner1.x < bezier_corner2.x - && corner2.x > bezier_corner1.x) - { - continue; - } - - // Original rotation axis - if bezier.line_test_crossings_prerotated(corner1, DMat2::IDENTITY, bezier).any(|intersection_point| { - let (_, y) = bezier.unrestricted_parametric_evaluate(intersection_point).into(); - y >= corner1.y && y <= corner2.y - }) { - return true; - } - if bezier.line_test_crossings_prerotated(corner2, DMat2::IDENTITY, bezier).any(|intersection_point| { - let (_, y) = bezier.unrestricted_parametric_evaluate(intersection_point).into(); - y >= corner1.y && y <= corner2.y - }) { - return true; - } - - // Perpendicular to original rotation axis - let rotated_bezier = bezier.apply_transformation(rotate_by_90deg); - if bezier.line_test_crossings_prerotated(corner1, DMat2::IDENTITY, rotated_bezier).any(|intersection_point| { - let (x, _) = bezier.unrestricted_parametric_evaluate(intersection_point).into(); - x >= corner1.x && x <= corner2.x - }) { - return true; - } - if bezier.line_test_crossings_prerotated(corner2, DMat2::IDENTITY, rotated_bezier).any(|intersection_point| { - let (x, _) = bezier.unrestricted_parametric_evaluate(intersection_point).into(); - x >= corner1.x && x <= corner2.x - }) { - return true; - } - } - - false - } - - /// Returns `true` if this subpath is completely inside the `other` subpath. - /// - pub fn is_inside_subpath(&self, other: &Subpath, error: Option, minimum_separation: Option) -> bool { - // Eliminate any possibility of one being inside the other, if either of them is empty - if self.is_empty() || other.is_empty() { - return false; - } - - // Safe to unwrap because the subpath is not empty - let inner_bbox = self.bounding_box().unwrap(); - let outer_bbox = other.bounding_box().unwrap(); - - // Eliminate this subpath if its bounding box is not completely inside the other subpath's bounding box. - // Reasoning: - // If the (min x, min y) of the inner subpath is less than or equal to the (min x, min y) of the outer subpath, - // or if the (min x, min y) of the inner subpath is greater than or equal to the (max x, max y) of the outer subpath, - // then the inner subpath is intersecting with or outside the outer subpath. The same logic applies for (max x, max y). - if !is_rectangle_inside_other(inner_bbox, outer_bbox) { - return false; - } - - // Eliminate this subpath if any of its anchors are outside the other subpath. - for anchors in self.anchors() { - if !other.contains_point(anchors) { - return false; - } - } - - // Eliminate this subpath if it intersects with the other subpath. - if !self.subpath_intersections(other, error, minimum_separation).is_empty() { - return false; - } - - // At this point: - // (1) This subpath's bounding box is inside the other subpath's bounding box, - // (2) Its anchors are inside the other subpath, and - // (3) It is not intersecting with the other subpath. - // Hence, this subpath is completely inside the given other subpath. - true - } - - /// Returns a normalized unit vector representing the tangent on the subpath based on the parametric `t`-value provided. - /// - pub fn tangent(&self, t: SubpathTValue) -> DVec2 { - let (segment_index, t) = self.t_value_to_parametric(t); - self.get_segment(segment_index).unwrap().tangent(TValue::Parametric(t)) - } - - /// Returns a normalized unit vector representing the direction of the normal on the subpath based on the parametric `t`-value provided. - /// - pub fn normal(&self, t: SubpathTValue) -> DVec2 { - let (segment_index, t) = self.t_value_to_parametric(t); - self.get_segment(segment_index).unwrap().normal(TValue::Parametric(t)) - } - - /// Returns two lists of `t`-values representing the local extrema of the `x` and `y` parametric subpaths respectively. - /// The list of `t`-values returned are filtered such that they fall within the range `[0, 1]`. - /// - pub fn local_extrema(&self) -> [Vec; 2] { - let number_of_curves = self.len_segments() as f64; - - // TODO: Consider the shared point between adjacent beziers. - self.iter().enumerate().fold([Vec::new(), Vec::new()], |mut acc, elem| { - let [x, y] = elem.1.local_extrema(); - // Convert t-values of bezier curve to t-values of subpath - acc[0].extend(x.map(|t| ((elem.0 as f64) + t) / number_of_curves).collect::>()); - acc[1].extend(y.map(|t| ((elem.0 as f64) + t) / number_of_curves).collect::>()); - acc - }) - } - - /// Return the min and max corners that represent the bounding box of the subpath. Return `None` if the subpath is empty. - /// - pub fn bounding_box(&self) -> Option<[DVec2; 2]> { - self.iter().map(|bezier| bezier.bounding_box()).reduce(|bbox1, bbox2| [bbox1[0].min(bbox2[0]), bbox1[1].max(bbox2[1])]) - } - - /// Return the min and max corners that represent the bounding box of the subpath, after a given affine transform. - pub fn bounding_box_with_transform(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> { - self.iter() - .map(|bezier| bezier.apply_transformation(|v| transform.transform_point2(v)).bounding_box()) - .reduce(|bbox1, bbox2| [bbox1[0].min(bbox2[0]), bbox1[1].max(bbox2[1])]) - } - - /// Return the min and max corners that represent the loose bounding box of the subpath (bounding box of all handles and anchors). - pub fn loose_bounding_box(&self) -> Option<[DVec2; 2]> { - self.manipulator_groups - .iter() - .flat_map(|group| [group.in_handle, group.out_handle, Some(group.anchor)]) - .flatten() - .map(|pos| [pos, pos]) - .reduce(|bbox1, bbox2| [bbox1[0].min(bbox2[0]), bbox1[1].max(bbox2[1])]) - } - - /// Return the min and max corners that represent the loose bounding box of the subpath, after a given affine transform. - pub fn loose_bounding_box_with_transform(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> { - self.manipulator_groups - .iter() - .flat_map(|group| [group.in_handle, group.out_handle, Some(group.anchor)]) - .flatten() - .map(|pos| transform.transform_point2(pos)) - .map(|pos| [pos, pos]) - .reduce(|bbox1, bbox2| [bbox1[0].min(bbox2[0]), bbox1[1].max(bbox2[1])]) - } - - /// Returns list of `t`-values representing the inflection points of the subpath. - /// The list of `t`-values returned are filtered such that they fall within the range `[0, 1]`. - /// - pub fn inflections(&self) -> Vec { - let number_of_curves = self.len_segments() as f64; - let inflection_t_values: Vec = self - .iter() - .enumerate() - .flat_map(|(index, bezier)| { - bezier - .inflections() - .into_iter() - // Convert t-values of bezier curve to t-values of subpath - .map(move |t| ((index as f64) + t) / number_of_curves) - }) - .collect(); - - // TODO: Consider the shared point between adjacent beziers. - inflection_t_values - } - - /// Does a path contain a point? Based on the non zero winding - pub fn contains_point(&self, target_point: DVec2) -> bool { - self.iter().map(|bezier| bezier.winding(target_point)).sum::() != 0 - } - - /// Does a path contain a point? Based on the non zero winding. Automatically adds a linear segment if the subpath is not closed. - pub fn contains_point_autoclose(&self, target_point: DVec2) -> bool { - let mut winding = self.iter().map(|bezier| bezier.winding(target_point)).sum::(); - if !self.closed { - if let [Some(first), Some(last)] = [self.manipulator_groups.first(), self.manipulator_groups.last()] { - winding += Bezier::from_linear_dvec2(first.anchor, last.anchor).winding(target_point); - } - } - - winding != 0 - } - - /// Randomly places points across the filled surface of this subpath (which is assumed to be closed). - /// The `separation_disk_diameter` determines the minimum distance between all points from one another. - /// Conceptually, this works by "throwing a dart" at the subpath's bounding box and keeping the dart only if: - /// - It's inside the shape - /// - It's not closer than `separation_disk_diameter` to any other point from a previous accepted dart throw - /// - /// This repeats until accepted darts fill all possible areas between one another. - /// - /// While the conceptual process described above asymptotically slows down and is never guaranteed to produce a maximal set in finite time, - /// this is implemented with an algorithm that produces a maximal set in O(n) time. The slowest part is actually checking if points are inside the subpath shape. - pub fn poisson_disk_points(&self, separation_disk_diameter: f64, rng: impl FnMut() -> f64, subpaths: &[(Self, [DVec2; 2])], subpath_index: usize) -> Vec { - let Some(bounding_box) = self.bounding_box() else { return Vec::new() }; - let (offset_x, offset_y) = bounding_box[0].into(); - let (width, height) = (bounding_box[1] - bounding_box[0]).into(); - - // TODO: Optimize the following code and make it more robust - - let mut shape = self.clone(); - shape.set_closed(true); - shape.apply_transform(DAffine2::from_translation((-offset_x, -offset_y).into())); - - let point_in_shape_checker = |point: DVec2| { - // Check against all paths the point is contained in to compute the correct winding number - let mut number = 0; - for (i, (shape, bb)) in subpaths.iter().enumerate() { - let point = point + bounding_box[0]; - if bb[0].x > point.x || bb[0].y > point.y || bb[1].x < point.x || bb[1].y < point.y { - continue; - } - let winding = shape.winding_order(point); - - if i == subpath_index && winding == 0 { - return false; - } - number += winding; - } - number != 0 - }; - - let square_edges_intersect_shape_checker = |corner1: DVec2, size: f64| { - let corner2 = corner1 + DVec2::splat(size); - self.rectangle_intersections_exist(corner1, corner2) - }; - - let mut points = crate::poisson_disk::poisson_disk_sample(width, height, separation_disk_diameter, point_in_shape_checker, square_edges_intersect_shape_checker, rng); - for point in &mut points { - point.x += offset_x; - point.y += offset_y; - } - points - } - - /// Returns the manipulator point that is needed for a miter join if it is possible. - /// - `miter_limit`: Defines a limit for the ratio between the miter length and the stroke width. - /// - /// Alternatively, this can be interpreted as limiting the angle that the miter can form. - /// When the limit is exceeded, no manipulator group will be returned. - /// This value should be greater than 0. If not, the default of 4 will be used. - pub fn miter_line_join(&self, other: &Subpath, miter_limit: Option) -> Option> { - let miter_limit = match miter_limit { - Some(miter_limit) if miter_limit > f64::EPSILON => miter_limit, - _ => 4., - }; - // TODO: Besides returning None using the `?` operator, is there a more appropriate way to handle a `None` result from `get_segment`? - let in_segment = self.get_segment(self.len_segments().checked_sub(1)?)?; - let out_segment = other.get_segment(0)?; - - let in_tangent = in_segment.tangent(TValue::Parametric(1.)); - let out_tangent = out_segment.tangent(TValue::Parametric(0.)); - - if in_tangent == DVec2::ZERO || out_tangent == DVec2::ZERO { - // Avoid panic from normalizing zero vectors - // TODO: Besides returning None, is there a more appropriate way to handle this? - return None; - } - let normalized_in_tangent = in_tangent.normalize(); - let normalized_out_tangent = out_tangent.normalize(); - - // The tangents must not be parallel for the miter join - if !normalized_in_tangent.abs_diff_eq(normalized_out_tangent, MAX_ABSOLUTE_DIFFERENCE) && !normalized_in_tangent.abs_diff_eq(-normalized_out_tangent, MAX_ABSOLUTE_DIFFERENCE) { - let intersection = line_intersection(in_segment.end(), in_tangent, out_segment.start(), out_tangent); - - let start_to_intersection = intersection - in_segment.end(); - let intersection_to_end = out_segment.start() - intersection; - if start_to_intersection == DVec2::ZERO || intersection_to_end == DVec2::ZERO { - // Avoid panic from normalizing zero vectors - // TODO: Besides returning None, is there a more appropriate way to handle this? - return None; - } - - // Draw the miter join if the intersection occurs in the correct direction with respect to the path - if start_to_intersection.normalize().abs_diff_eq(in_tangent, MAX_ABSOLUTE_DIFFERENCE) - && intersection_to_end.normalize().abs_diff_eq(out_tangent, MAX_ABSOLUTE_DIFFERENCE) - && miter_limit > f64::EPSILON / (start_to_intersection.angle_to(-intersection_to_end).abs() / 2.).sin() - { - return Some(ManipulatorGroup { - anchor: intersection, - in_handle: None, - out_handle: None, - id: PointId::new(), - }); - } - } - // If we can't draw the miter join, default to a bevel join - None - } - - /// Returns the necessary information to create a round join with the provided center. - /// The returned items correspond to: - /// - The `out_handle` for the last manipulator group of `self` - /// - The new manipulator group to be added - /// - The `in_handle` for the first manipulator group of `other` - pub fn round_line_join(&self, other: &Subpath, center: DVec2) -> (DVec2, ManipulatorGroup, DVec2) { - let left = self.manipulator_groups[self.len() - 1].anchor; - let right = other.manipulator_groups[0].anchor; - - let center_to_right = right - center; - let center_to_left = left - center; - - let in_segment = self.len_segments().checked_sub(1).and_then(|segment| self.get_segment(segment)); - let in_tangent = in_segment.map(|in_segment| in_segment.tangent(TValue::Parametric(1.))); - - let mut angle = center_to_right.angle_to(center_to_left) / 2.; - let mut arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right); - - if in_tangent.map(|in_tangent| (arc_point - left).angle_to(in_tangent).abs()).unwrap_or_default() > PI / 2. { - angle = angle - PI * (if angle < 0. { -1. } else { 1. }); - arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right); - } - - compute_circular_subpath_details(left, arc_point, right, center, Some(angle)) - } - - /// Returns the necessary information to create a round cap between the end of `self` and the beginning of `other`. - /// The returned items correspond to: - /// - The `out_handle` for the last manipulator group of `self` - /// - The new manipulator group to be added - /// - The `in_handle` for the first manipulator group of `other` - pub(crate) fn round_cap(&self, other: &Subpath) -> (DVec2, ManipulatorGroup, DVec2) { - let left = self.manipulator_groups[self.len() - 1].anchor; - let right = other.manipulator_groups[0].anchor; - - let center = (right + left) / 2.; - let center_to_right = right - center; - - let arc_point = center + center_to_right.perp(); - - compute_circular_subpath_details(left, arc_point, right, center, None) - } - - /// Returns the two manipulator groups that create a square cap between the end of `self` and the beginning of `other`. - pub(crate) fn square_cap(&self, other: &Subpath) -> [ManipulatorGroup; 2] { - let left = self.manipulator_groups[self.len() - 1].anchor; - let right = other.manipulator_groups[0].anchor; - - let center = (right + left) / 2.; - let center_to_right = right - center; - - let translation = center_to_right.perp(); - - [ManipulatorGroup::new_anchor(left + translation), ManipulatorGroup::new_anchor(right + translation)] - } - - /// Returns the curvature, a scalar value for the derivative at the point `t` along the subpath. - /// Curvature is 1 over the radius of a circle with an equivalent derivative. - /// - pub fn curvature(&self, t: SubpathTValue) -> f64 { - let (segment_index, t) = self.t_value_to_parametric(t); - self.get_segment(segment_index).unwrap().curvature(TValue::Parametric(t)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::Bezier; - use crate::consts::MAX_ABSOLUTE_DIFFERENCE; - use crate::utils; - use glam::DVec2; - - fn normalize_t(n: i64, t: f64) -> f64 { - t * (n as f64) % 1. - } - - #[test] - fn evaluate_one_subpath_curve() { - let start = DVec2::new(20., 30.); - let end = DVec2::new(60., 45.); - let handle = DVec2::new(75., 85.); - - let bezier = Bezier::from_quadratic_dvec2(start, handle, end); - let subpath = Subpath::new( - vec![ - ManipulatorGroup { - anchor: start, - in_handle: None, - out_handle: Some(handle), - id: EmptyId, - }, - ManipulatorGroup { - anchor: end, - in_handle: None, - out_handle: Some(handle), - id: EmptyId, - }, - ], - false, - ); - - let t0 = 0.; - assert_eq!(subpath.evaluate(SubpathTValue::GlobalParametric(t0)), bezier.evaluate(TValue::Parametric(t0))); - - let t1 = 0.25; - assert_eq!(subpath.evaluate(SubpathTValue::GlobalParametric(t1)), bezier.evaluate(TValue::Parametric(t1))); - - let t2 = 0.50; - assert_eq!(subpath.evaluate(SubpathTValue::GlobalParametric(t2)), bezier.evaluate(TValue::Parametric(t2))); - - let t3 = 1.; - assert_eq!(subpath.evaluate(SubpathTValue::GlobalParametric(t3)), bezier.evaluate(TValue::Parametric(t3))); - } - - #[test] - fn evaluate_multiple_subpath_curves() { - let start = DVec2::new(20., 30.); - let middle = DVec2::new(70., 70.); - let end = DVec2::new(60., 45.); - let handle1 = DVec2::new(75., 85.); - let handle2 = DVec2::new(40., 30.); - let handle3 = DVec2::new(10., 10.); - - let linear_bezier = Bezier::from_linear_dvec2(start, middle); - let quadratic_bezier = Bezier::from_quadratic_dvec2(middle, handle1, end); - let cubic_bezier = Bezier::from_cubic_dvec2(end, handle2, handle3, start); - - let mut subpath = Subpath::new( - vec![ - ManipulatorGroup { - anchor: start, - in_handle: Some(handle3), - out_handle: None, - id: EmptyId, - }, - ManipulatorGroup { - anchor: middle, - in_handle: None, - out_handle: Some(handle1), - id: EmptyId, - }, - ManipulatorGroup { - anchor: end, - in_handle: None, - out_handle: Some(handle2), - id: EmptyId, - }, - ], - false, - ); - - // Test open subpath - - let mut n = (subpath.len() as i64) - 1; - - let t0 = 0.; - assert!( - utils::dvec2_compare( - subpath.evaluate(SubpathTValue::GlobalParametric(t0)), - linear_bezier.evaluate(TValue::Parametric(normalize_t(n, t0))), - MAX_ABSOLUTE_DIFFERENCE - ) - .all() - ); - - let t1 = 0.25; - assert!( - utils::dvec2_compare( - subpath.evaluate(SubpathTValue::GlobalParametric(t1)), - linear_bezier.evaluate(TValue::Parametric(normalize_t(n, t1))), - MAX_ABSOLUTE_DIFFERENCE - ) - .all() - ); - - let t2 = 0.50; - assert!( - utils::dvec2_compare( - subpath.evaluate(SubpathTValue::GlobalParametric(t2)), - quadratic_bezier.evaluate(TValue::Parametric(normalize_t(n, t2))), - MAX_ABSOLUTE_DIFFERENCE - ) - .all() - ); - - let t3 = 0.75; - assert!( - utils::dvec2_compare( - subpath.evaluate(SubpathTValue::GlobalParametric(t3)), - quadratic_bezier.evaluate(TValue::Parametric(normalize_t(n, t3))), - MAX_ABSOLUTE_DIFFERENCE - ) - .all() - ); - - let t4 = 1.; - assert!( - utils::dvec2_compare( - subpath.evaluate(SubpathTValue::GlobalParametric(t4)), - quadratic_bezier.evaluate(TValue::Parametric(1.)), - MAX_ABSOLUTE_DIFFERENCE - ) - .all() - ); - - // Test closed subpath - - subpath.closed = true; - n = subpath.len() as i64; - - let t5 = 2. / 3.; - assert!( - utils::dvec2_compare( - subpath.evaluate(SubpathTValue::GlobalParametric(t5)), - cubic_bezier.evaluate(TValue::Parametric(normalize_t(n, t5))), - MAX_ABSOLUTE_DIFFERENCE - ) - .all() - ); - - let t6 = 1.; - assert!( - utils::dvec2_compare( - subpath.evaluate(SubpathTValue::GlobalParametric(t6)), - cubic_bezier.evaluate(TValue::Parametric(1.)), - MAX_ABSOLUTE_DIFFERENCE - ) - .all() - ); - } - - #[test] - fn intersection_linear_multiple_subpath_curves_test_one() { - // M 35 125 C 40 40 120 120 43 43 Q 175 90 145 150 Q 70 185 35 125 Z - - let cubic_start = DVec2::new(35., 125.); - let cubic_handle_1 = DVec2::new(40., 40.); - let cubic_handle_2 = DVec2::new(120., 120.); - let cubic_end = DVec2::new(43., 43.); - - let quadratic_1_handle = DVec2::new(175., 90.); - let quadratic_end = DVec2::new(145., 150.); - - let quadratic_2_handle = DVec2::new(70., 185.); - - let cubic_bezier = Bezier::from_cubic_dvec2(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end); - let quadratic_bezier_1 = Bezier::from_quadratic_dvec2(cubic_end, quadratic_1_handle, quadratic_end); - - let subpath = Subpath::new( - vec![ - ManipulatorGroup { - anchor: cubic_start, - in_handle: None, - out_handle: Some(cubic_handle_1), - id: EmptyId, - }, - ManipulatorGroup { - anchor: cubic_end, - in_handle: Some(cubic_handle_2), - out_handle: None, - id: EmptyId, - }, - ManipulatorGroup { - anchor: quadratic_end, - in_handle: Some(quadratic_1_handle), - out_handle: Some(quadratic_2_handle), - id: EmptyId, - }, - ], - true, - ); - - let line = Bezier::from_linear_coordinates(150., 150., 20., 20.); - - let cubic_intersections = cubic_bezier.intersections(&line, None, None); - let quadratic_1_intersections = quadratic_bezier_1.intersections(&line, None, None); - let subpath_intersections = subpath.intersections(&line, None, None); - - assert!( - utils::dvec2_compare( - cubic_bezier.evaluate(TValue::Parametric(cubic_intersections[0])), - subpath.evaluate(SubpathTValue::Parametric { - segment_index: subpath_intersections[0].0, - t: subpath_intersections[0].1 - }), - MAX_ABSOLUTE_DIFFERENCE - ) - .all() - ); - - assert!( - utils::dvec2_compare( - quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[0])), - subpath.evaluate(SubpathTValue::Parametric { - segment_index: subpath_intersections[1].0, - t: subpath_intersections[1].1 - }), - MAX_ABSOLUTE_DIFFERENCE - ) - .all() - ); - - assert!( - utils::dvec2_compare( - quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[1])), - subpath.evaluate(SubpathTValue::Parametric { - segment_index: subpath_intersections[2].0, - t: subpath_intersections[2].1 - }), - MAX_ABSOLUTE_DIFFERENCE - ) - .all() - ); - } - - #[test] - fn intersection_linear_multiple_subpath_curves_test_two() { - // M34 107 C40 40 120 120 102 29 Q175 90 129 171 Q70 185 34 107 Z - // M150 150 L 20 20 - - let cubic_start = DVec2::new(34., 107.); - let cubic_handle_1 = DVec2::new(40., 40.); - let cubic_handle_2 = DVec2::new(120., 120.); - let cubic_end = DVec2::new(102., 29.); - - let quadratic_1_handle = DVec2::new(175., 90.); - let quadratic_end = DVec2::new(129., 171.); - - let quadratic_2_handle = DVec2::new(70., 185.); - - let cubic_bezier = Bezier::from_cubic_dvec2(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end); - let quadratic_bezier_1 = Bezier::from_quadratic_dvec2(cubic_end, quadratic_1_handle, quadratic_end); - - let subpath = Subpath::new( - vec![ - ManipulatorGroup { - anchor: cubic_start, - in_handle: None, - out_handle: Some(cubic_handle_1), - id: EmptyId, - }, - ManipulatorGroup { - anchor: cubic_end, - in_handle: Some(cubic_handle_2), - out_handle: None, - id: EmptyId, - }, - ManipulatorGroup { - anchor: quadratic_end, - in_handle: Some(quadratic_1_handle), - out_handle: Some(quadratic_2_handle), - id: EmptyId, - }, - ], - true, - ); - - let line = Bezier::from_linear_coordinates(150., 150., 20., 20.); - - let cubic_intersections = cubic_bezier.intersections(&line, None, None); - let quadratic_1_intersections = quadratic_bezier_1.intersections(&line, None, None); - let subpath_intersections = subpath.intersections(&line, None, None); - - assert!( - utils::dvec2_compare( - cubic_bezier.evaluate(TValue::Parametric(cubic_intersections[0])), - subpath.evaluate(SubpathTValue::Parametric { - segment_index: subpath_intersections[0].0, - t: subpath_intersections[0].1 - }), - MAX_ABSOLUTE_DIFFERENCE - ) - .all() - ); - - assert!( - utils::dvec2_compare( - quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[0])), - subpath.evaluate(SubpathTValue::Parametric { - segment_index: subpath_intersections[1].0, - t: subpath_intersections[1].1 - }), - MAX_ABSOLUTE_DIFFERENCE - ) - .all() - ); - } - - #[test] - fn intersection_linear_multiple_subpath_curves_test_three() { - // M35 125 C40 40 120 120 44 44 Q175 90 145 150 Q70 185 35 125 Z - - let cubic_start = DVec2::new(35., 125.); - let cubic_handle_1 = DVec2::new(40., 40.); - let cubic_handle_2 = DVec2::new(120., 120.); - let cubic_end = DVec2::new(44., 44.); - - let quadratic_1_handle = DVec2::new(175., 90.); - let quadratic_end = DVec2::new(145., 150.); - - let quadratic_2_handle = DVec2::new(70., 185.); - - let cubic_bezier = Bezier::from_cubic_dvec2(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end); - let quadratic_bezier_1 = Bezier::from_quadratic_dvec2(cubic_end, quadratic_1_handle, quadratic_end); - - let subpath = Subpath::new( - vec![ - ManipulatorGroup { - anchor: cubic_start, - in_handle: None, - out_handle: Some(cubic_handle_1), - id: EmptyId, - }, - ManipulatorGroup { - anchor: cubic_end, - in_handle: Some(cubic_handle_2), - out_handle: None, - id: EmptyId, - }, - ManipulatorGroup { - anchor: quadratic_end, - in_handle: Some(quadratic_1_handle), - out_handle: Some(quadratic_2_handle), - id: EmptyId, - }, - ], - true, - ); - - let line = Bezier::from_linear_coordinates(150., 150., 20., 20.); - - let cubic_intersections = cubic_bezier.intersections(&line, None, None); - let quadratic_1_intersections = quadratic_bezier_1.intersections(&line, None, None); - let subpath_intersections = subpath.intersections(&line, None, None); - - assert!( - utils::dvec2_compare( - cubic_bezier.evaluate(TValue::Parametric(cubic_intersections[0])), - subpath.evaluate(SubpathTValue::Parametric { - segment_index: subpath_intersections[0].0, - t: subpath_intersections[0].1 - }), - MAX_ABSOLUTE_DIFFERENCE - ) - .all() - ); - - assert!( - utils::dvec2_compare( - quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[0])), - subpath.evaluate(SubpathTValue::Parametric { - segment_index: subpath_intersections[1].0, - t: subpath_intersections[1].1 - }), - MAX_ABSOLUTE_DIFFERENCE - ) - .all() - ); - - assert!( - utils::dvec2_compare( - quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[1])), - subpath.evaluate(SubpathTValue::Parametric { - segment_index: subpath_intersections[2].0, - t: subpath_intersections[2].1 - }), - MAX_ABSOLUTE_DIFFERENCE - ) - .all() - ); - } - - // TODO: add more intersection tests - - #[test] - fn is_inside_subpath() { - let boundary_polygon = [DVec2::new(100., 100.), DVec2::new(500., 100.), DVec2::new(500., 500.), DVec2::new(100., 500.)].to_vec(); - let boundary_polygon = Subpath::from_anchors_linear(boundary_polygon, true); - - let curve = Bezier::from_quadratic_dvec2(DVec2::new(189., 289.), DVec2::new(9., 286.), DVec2::new(45., 410.)); - let curve_intersecting = Subpath::::from_bezier(&curve); - assert!(!curve_intersecting.is_inside_subpath(&boundary_polygon, None, None)); - - let curve = Bezier::from_quadratic_dvec2(DVec2::new(115., 37.), DVec2::new(51.4, 91.8), DVec2::new(76.5, 242.)); - let curve_outside = Subpath::::from_bezier(&curve); - assert!(!curve_outside.is_inside_subpath(&boundary_polygon, None, None)); - - let curve = Bezier::from_cubic_dvec2(DVec2::new(210.1, 133.5), DVec2::new(150.2, 436.9), DVec2::new(436., 285.), DVec2::new(247.6, 240.7)); - let curve_inside = Subpath::::from_bezier(&curve); - assert!(curve_inside.is_inside_subpath(&boundary_polygon, None, None)); - - let line = Bezier::from_linear_dvec2(DVec2::new(101., 101.5), DVec2::new(150.2, 499.)); - let line_inside = Subpath::::from_bezier(&line); - assert!(line_inside.is_inside_subpath(&boundary_polygon, None, None)); - } - - #[test] - fn round_join_counter_clockwise_rotation() { - // Test case where the round join is drawn in the counter clockwise direction between two consecutive offsets - let subpath = Subpath::new( - vec![ - ManipulatorGroup { - anchor: DVec2::new(20., 20.), - out_handle: Some(DVec2::new(10., 90.)), - in_handle: None, - id: EmptyId, - }, - ManipulatorGroup { - anchor: DVec2::new(114., 159.), - out_handle: None, - in_handle: Some(DVec2::new(60., 40.)), - id: EmptyId, - }, - ManipulatorGroup { - anchor: DVec2::new(148., 155.), - out_handle: None, - in_handle: None, - id: EmptyId, - }, - ], - false, - ); - - let offset = subpath.offset(10., utils::Join::Round); - let offset_len = offset.len(); - - let manipulator_groups = offset.manipulator_groups(); - let round_start = manipulator_groups[offset_len - 4].anchor; - let round_point = manipulator_groups[offset_len - 3].anchor; - let round_end = manipulator_groups[offset_len - 2].anchor; - - let middle = (round_start + round_end) / 2.; - - assert!((round_point - middle).angle_to(round_start - middle) > 0.); - assert!((round_end - middle).angle_to(round_point - middle) > 0.); - } - - #[test] - fn round_join_clockwise_rotation() { - // Test case where the round join is drawn in the clockwise direction between two consecutive offsets - let subpath = Subpath::new( - vec![ - ManipulatorGroup { - anchor: DVec2::new(20., 20.), - out_handle: Some(DVec2::new(10., 90.)), - in_handle: None, - id: EmptyId, - }, - ManipulatorGroup { - anchor: DVec2::new(150., 40.), - out_handle: None, - in_handle: Some(DVec2::new(60., 40.)), - id: EmptyId, - }, - ManipulatorGroup { - anchor: DVec2::new(78., 36.), - out_handle: None, - in_handle: None, - id: EmptyId, - }, - ], - false, - ); - - let offset = subpath.offset(-15., utils::Join::Round); - let offset_len = offset.len(); - - let manipulator_groups = offset.manipulator_groups(); - let round_start = manipulator_groups[offset_len - 4].anchor; - let round_point = manipulator_groups[offset_len - 3].anchor; - let round_end = manipulator_groups[offset_len - 2].anchor; - - let middle = (round_start + round_end) / 2.; - - assert!((round_point - middle).angle_to(round_start - middle) < 0.); - assert!((round_end - middle).angle_to(round_point - middle) < 0.); - } -} diff --git a/libraries/bezier-rs/src/subpath/structs.rs b/libraries/bezier-rs/src/subpath/structs.rs deleted file mode 100644 index f0ef24dd7d..0000000000 --- a/libraries/bezier-rs/src/subpath/structs.rs +++ /dev/null @@ -1,146 +0,0 @@ -use super::Bezier; -use glam::{DAffine2, DVec2}; -use std::fmt::{Debug, Formatter, Result}; -use std::hash::Hash; - -/// An id type used for each [ManipulatorGroup]. -pub trait Identifier: Sized + Clone + PartialEq + Hash + 'static { - fn new() -> Self; -} - -/// An empty id type for use in tests -#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] -#[cfg(test)] -pub(crate) struct EmptyId; - -#[cfg(test)] -impl Identifier for EmptyId { - fn new() -> Self { - Self - } -} - -/// Structure used to represent a single anchor with up to two optional associated handles along a `Subpath` -#[derive(Copy, Clone, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct ManipulatorGroup { - pub anchor: DVec2, - pub in_handle: Option, - pub out_handle: Option, - pub id: PointId, -} - -// TODO: Remove once we no longer need to hash floats in Graphite -impl Hash for ManipulatorGroup { - fn hash(&self, state: &mut H) { - self.anchor.to_array().iter().for_each(|x| x.to_bits().hash(state)); - self.in_handle.is_some().hash(state); - if let Some(in_handle) = self.in_handle { - in_handle.to_array().iter().for_each(|x| x.to_bits().hash(state)); - } - self.out_handle.is_some().hash(state); - if let Some(out_handle) = self.out_handle { - out_handle.to_array().iter().for_each(|x| x.to_bits().hash(state)); - } - self.id.hash(state); - } -} - -#[cfg(feature = "dyn-any")] -unsafe impl dyn_any::StaticType for ManipulatorGroup { - type Static = ManipulatorGroup; -} - -impl Debug for ManipulatorGroup { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - f.debug_struct("ManipulatorGroup") - .field("anchor", &self.anchor) - .field("in_handle", &self.in_handle) - .field("out_handle", &self.out_handle) - .finish() - } -} - -impl ManipulatorGroup { - /// Construct a new manipulator group from an anchor, in handle and out handle - pub fn new(anchor: DVec2, in_handle: Option, out_handle: Option) -> Self { - let id = PointId::new(); - Self { anchor, in_handle, out_handle, id } - } - - /// Construct a new manipulator point with just an anchor position - pub fn new_anchor(anchor: DVec2) -> Self { - Self::new(anchor, Some(anchor), Some(anchor)) - } - - pub fn new_anchor_linear(anchor: DVec2) -> Self { - Self::new(anchor, None, None) - } - - /// Construct a new manipulator group from an anchor, in handle, out handle and an id - pub fn new_with_id(anchor: DVec2, in_handle: Option, out_handle: Option, id: PointId) -> Self { - Self { anchor, in_handle, out_handle, id } - } - - /// Construct a new manipulator point with just an anchor position and an id - pub fn new_anchor_with_id(anchor: DVec2, id: PointId) -> Self { - Self::new_with_id(anchor, Some(anchor), Some(anchor), id) - } - - /// Create a bezier curve that starts at the current manipulator group and finishes in the `end_group` manipulator group. - pub fn to_bezier(&self, end_group: &ManipulatorGroup) -> Bezier { - let start = self.anchor; - let end = end_group.anchor; - let out_handle = self.out_handle; - let in_handle = end_group.in_handle; - - match (out_handle, in_handle) { - (Some(handle1), Some(handle2)) => Bezier::from_cubic_dvec2(start, handle1, handle2, end), - (Some(handle), None) | (None, Some(handle)) => Bezier::from_quadratic_dvec2(start, handle, end), - (None, None) => Bezier::from_linear_dvec2(start, end), - } - } - - /// Apply a transformation to all of the [ManipulatorGroup] points - pub fn apply_transform(&mut self, affine_transform: DAffine2) { - self.anchor = affine_transform.transform_point2(self.anchor); - self.in_handle = self.in_handle.map(|in_handle| affine_transform.transform_point2(in_handle)); - self.out_handle = self.out_handle.map(|out_handle| affine_transform.transform_point2(out_handle)); - } - - /// Are all handles at finite positions - pub fn is_finite(&self) -> bool { - self.anchor.is_finite() && self.in_handle.is_none_or(|handle| handle.is_finite()) && self.out_handle.is_none_or(|handle| handle.is_finite()) - } - - /// Reverse directions of handles - pub fn flip(mut self) -> Self { - std::mem::swap(&mut self.in_handle, &mut self.out_handle); - self - } - - pub fn has_in_handle(&self) -> bool { - self.in_handle.map(|handle| Self::has_handle(self.anchor, handle)).unwrap_or(false) - } - - pub fn has_out_handle(&self) -> bool { - self.out_handle.map(|handle| Self::has_handle(self.anchor, handle)).unwrap_or(false) - } - - fn has_handle(anchor: DVec2, handle: DVec2) -> bool { - !((handle.x - anchor.x).abs() < f64::EPSILON && (handle.y - anchor.y).abs() < f64::EPSILON) - } -} - -#[derive(Copy, Clone)] -pub enum AppendType { - IgnoreStart, - SmoothJoin(f64), -} - -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub enum ArcType { - Open, - Closed, - PieSlice, -} diff --git a/libraries/bezier-rs/src/subpath/transform.rs b/libraries/bezier-rs/src/subpath/transform.rs deleted file mode 100644 index 88fc4612f3..0000000000 --- a/libraries/bezier-rs/src/subpath/transform.rs +++ /dev/null @@ -1,1115 +0,0 @@ -use super::*; -use crate::BezierHandles; -use crate::consts::MAX_ABSOLUTE_DIFFERENCE; -use crate::utils::{Cap, Join, SubpathTValue, TValue}; -use glam::{DAffine2, DVec2}; -use std::vec; - -/// Helper function to ensure the index and t value pair is mapped within a maximum index value. -/// Allows for the point to be fetched without needing to handle an additional edge case. -/// - Ex. Via `subpath.iter().nth(index).evaluate(t);` -fn map_index_within_range(index: usize, t: f64, max_size: usize) -> (usize, f64) { - if max_size > 0 && index == max_size && t == 0. { (index - 1, 1.) } else { (index, t) } -} - -/// Functionality that transforms Subpaths, such as split, reduce, offset, etc. -impl Subpath { - /// Returns either one or two Subpaths that result from splitting the original Subpath at the point corresponding to `t`. - /// If the original Subpath was closed, a single open Subpath will be returned. - /// If the original Subpath was open, two open Subpaths will be returned. - /// - pub fn split(&self, t: SubpathTValue) -> (Subpath, Option>) { - let (segment_index, t) = self.t_value_to_parametric(t); - let curve = self.get_segment(segment_index).unwrap(); - - let [first_bezier, second_bezier] = curve.split(TValue::Parametric(t)); - - let mut clone = self.manipulator_groups.clone(); - // Split the manipulator group list such that the split location is between the last and first elements of the two split halves - // If the split is on an anchor point, include this anchor point in the first half of the split, except for the first manipulator group which we want in the second group - let (mut first_split, mut second_split) = if !(t == 0. && segment_index == 0) { - let clone2 = clone.split_off(self.len().min(segment_index + 1 + (t == 1.) as usize)); - (clone, clone2) - } else { - (vec![], clone) - }; - - // If the subpath is closed and the split point is the start or end of the Subpath - if self.closed && ((t == 0. && segment_index == 0) || (t == 1. && segment_index == self.len_segments() - 1)) { - // The entire vector of manipulator groups will be in the second_split - // Add a new manipulator group with the same anchor as the first node to represent the end of the now opened subpath - let last_curve = self.iter().last().unwrap(); - first_split.push(ManipulatorGroup { - anchor: first_bezier.end(), - in_handle: last_curve.handle_end(), - out_handle: None, - id: PointId::new(), - }); - } else { - if !first_split.is_empty() { - let num_elements = first_split.len(); - first_split[num_elements - 1].out_handle = first_bezier.handle_start(); - } - - if !second_split.is_empty() { - second_split[0].in_handle = second_bezier.handle_end(); - } - - // Push new manipulator groups to represent the location of the split at the end of the first group and at the start of the second - // If the split was at a manipulator group's anchor, add only one manipulator group - // Add it to the first list when the split location is on the first manipulator group, otherwise add to the second list - if (t % 1. != 0.) || segment_index == 0 { - first_split.push(ManipulatorGroup { - anchor: first_bezier.end(), - in_handle: first_bezier.handle_end(), - out_handle: None, - id: PointId::new(), - }); - } - - if !(t == 0. && segment_index == 0) { - second_split.insert( - 0, - ManipulatorGroup { - anchor: second_bezier.start(), - in_handle: None, - out_handle: second_bezier.handle_start(), - id: PointId::new(), - }, - ); - } - } - - if self.closed { - // "Rotate" the manipulator groups list so that the split point becomes the start and end of the open subpath - second_split.append(&mut first_split); - (Subpath::new(second_split, false), None) - } else { - (Subpath::new(first_split, false), Some(Subpath::new(second_split, false))) - } - } - - /// Returns [ManipulatorGroup]s with a reversed winding order. - fn reverse_manipulator_groups(manipulator_groups: &[ManipulatorGroup]) -> Vec> { - manipulator_groups - .iter() - .rev() - .map(|group| ManipulatorGroup { - anchor: group.anchor, - in_handle: group.out_handle, - out_handle: group.in_handle, - id: PointId::new(), - }) - .collect::>>() - } - - /// Returns a [Subpath] with a reversed winding order. - /// Note that a reversed closed subpath will start on the same manipulator group and simply wind the other direction - pub fn reverse(&self) -> Subpath { - let mut reversed = Subpath::reverse_manipulator_groups(self.manipulator_groups()); - if self.closed { - reversed.rotate_right(1); - }; - Subpath { - manipulator_groups: reversed, - closed: self.closed, - } - } - - /// Returns an open [Subpath] that results from trimming the original Subpath between the points corresponding to `t1` and `t2`, maintaining the winding order of the original. - /// If the original Subpath is closed, the order of arguments does matter. - /// The resulting Subpath will wind from the given `t1` to `t2`. - /// That means, if the value of `t1` > `t2`, it will cross the break between endpoints from `t1` to `t = 1 = 0` to `t2`. - /// If a path winding in the reverse direction is desired, call `trim` on the `Subpath` returned from `Subpath::reverse`. - /// - pub fn trim(&self, t1: SubpathTValue, t2: SubpathTValue) -> Subpath { - // Return a clone of the Subpath if it is not long enough to be a valid Bezier - if self.manipulator_groups.is_empty() { - return Subpath { - manipulator_groups: vec![], - closed: self.closed, - }; - } - - let (mut t1_curve_index, mut t1_curve_t) = self.t_value_to_parametric(t1); - let (mut t2_curve_index, mut t2_curve_t) = self.t_value_to_parametric(t2); - - // The only case where t would be 1 is when the input parameter refers to the the very last point on the subpath. - // We want these index and t pairs to always represent that point as the next curve index with t == 0. - if t1_curve_t == 1. { - t1_curve_index += 1; - t1_curve_t = 0.; - } - if t2_curve_t == 1. { - t2_curve_index += 1; - t2_curve_t = 0.; - } - - // Check if the trimmed result is in the reverse direction - let are_arguments_reversed = t1_curve_index > t2_curve_index || (t1_curve_index == t2_curve_index && t1_curve_t > t2_curve_t); - if !self.closed && are_arguments_reversed { - (t1_curve_index, t2_curve_index) = (t2_curve_index, t1_curve_index); - (t1_curve_t, t2_curve_t) = (t2_curve_t, t1_curve_t); - } - - // Get a new list from the manipulator groups that will be trimmed at the ends to form the resulting subpath. - // The list will contain enough manipulator groups such that the later code simply needs to trim the first and last bezier segments - // and then update the values of the corresponding first and last manipulator groups accordingly. - let mut cloned_manipulator_groups = self.manipulator_groups.clone(); - let mut new_manipulator_groups = if self.closed && are_arguments_reversed { - // Need to rotate the cloned manipulator groups vector - // Remove the elements starting from t1_curve_index to become the new beginning of the list - let mut front = cloned_manipulator_groups.split_off(t1_curve_index); - // Truncate middle elements that are not needed - cloned_manipulator_groups.truncate(t2_curve_index + ((t2_curve_t != 0.) as usize) + 1); - // Reconnect the two ends in the new order - front.extend(cloned_manipulator_groups); - if t1_curve_index == t2_curve_index % self.len_segments() { - // If the start and end of the trim are in the same bezier segment, we want to add a duplicate of the first two manipulator groups. - // This is to make sure the the closed loop is correctly represented and because this segment needs to be trimmed on both ends of the resulting subpath. - front.push(front[0].clone()); - front.push(front[1].clone()); - } - if t1_curve_index == t2_curve_index % self.len_segments() + 1 { - // If the start and end of the trim are in adjacent bezier segments, we want to add a duplicate of the first manipulator group. - // This is to make sure the the closed loop is correctly represented. - front.push(front[0].clone()); - } - front - } else { - // Determine the subsection of the subpath's manipulator groups that are needed - if self.closed { - // Add a duplicate of the first manipulator group to ensure the final closing segment is considered - cloned_manipulator_groups.push(cloned_manipulator_groups[0].clone()); - } - - // Find the start and end of the new range and consider whether the indices are reversed - let range_start = t1_curve_index.min(t2_curve_index); - // Add 1 since the drain range is not inclusive - // Add 1 again if the corresponding t is not 0 because we want to include the next manipulator group which forms the bezier that this t value is on - let range_end = 1 + t2_curve_index + ((t2_curve_t != 0.) as usize); - - cloned_manipulator_groups - .drain(range_start..range_end.min(cloned_manipulator_groups.len())) - .collect::>>() - }; - - // Adjust curve indices to match the cloned list - if self.closed && are_arguments_reversed { - // If trimmed subpath required rotating the manipulator group, adjust the indices to match - t2_curve_index = (t2_curve_index + self.len_segments() - t1_curve_index) % self.len_segments(); - if t2_curve_index == 0 { - // If the case is where the start and end are in the same bezier, - // change the index to point to the duplicate of this bezier that was pushed to the vector - t2_curve_index += self.len_segments(); - } - t1_curve_index = 0; - } else { - let min_index = t1_curve_index.min(t2_curve_index); - t1_curve_index -= min_index; - t2_curve_index -= min_index; - } - - // Change the representation of the point corresponding to the end point of the subpath - // So that we do not need an additional edges case in the later code to handle this point - (t1_curve_index, t1_curve_t) = map_index_within_range(t1_curve_index, t1_curve_t, new_manipulator_groups.len() - 1); - (t2_curve_index, t2_curve_t) = map_index_within_range(t2_curve_index, t2_curve_t, new_manipulator_groups.len() - 1); - - if new_manipulator_groups.len() == 1 { - // This case will occur when `t1` and `t2` both represent one of the manipulator group anchors - // Add a duplicate manipulator group so that the returned Subpath is still a valid Bezier - let mut point = new_manipulator_groups[0].clone(); - point.in_handle = None; - point.out_handle = None; - return Subpath { - manipulator_groups: vec![point], - closed: false, - }; - } - - let len_new_manip_groups = new_manipulator_groups.len(); - - // Create Beziers from the first and last pairs of manipulator groups - // These will be trimmed to form the start and end of the new subpath - let curve1 = new_manipulator_groups[0].to_bezier(&new_manipulator_groups[1]); - let curve2 = new_manipulator_groups[len_new_manip_groups - 2].to_bezier(&new_manipulator_groups[len_new_manip_groups - 1]); - - // If the target curve_indices are the same, then the trim must be happening within one bezier - // This means curve1 == curve2 must be true, and we can simply call the Bezier trim. - if t1_curve_index == t2_curve_index { - return Subpath::from_bezier(&curve1.trim(TValue::Parametric(t1_curve_t), TValue::Parametric(t2_curve_t))); - } - - // Split the bezier's with the according t value and keep the correct half - let [_, front_split] = curve1.split(TValue::Parametric(t1_curve_t)); - let [back_split, _] = curve2.split(TValue::Parametric(t2_curve_t)); - - // Update the first two manipulator groups to match the front_split - new_manipulator_groups[1].in_handle = front_split.handle_end(); - new_manipulator_groups[0] = ManipulatorGroup { - anchor: front_split.start(), - in_handle: None, - out_handle: front_split.handle_start(), - id: PointId::new(), - }; - - // Update the last two manipulator groups to match the back_split - new_manipulator_groups[len_new_manip_groups - 2].out_handle = back_split.handle_start(); - new_manipulator_groups[len_new_manip_groups - 1] = ManipulatorGroup { - anchor: back_split.end(), - in_handle: back_split.handle_end(), - out_handle: None, - id: PointId::new(), - }; - - Subpath { - manipulator_groups: new_manipulator_groups, - closed: false, - } - } - - /// Apply a transformation to all of the [ManipulatorGroup]s in the [Subpath]. - pub fn apply_transform(&mut self, affine_transform: DAffine2) { - for manipulator_group in &mut self.manipulator_groups { - manipulator_group.apply_transform(affine_transform); - } - } - - /// Smooths a Subpath up to the first derivative, using a weighted averaged based on segment length. - /// The Subpath must be open, and contain no quadratic segments. - pub(crate) fn smooth_open_subpath(&mut self) { - if self.len() < 2 { - return; - } - for i in 1..self.len() - 1 { - let first_bezier = self.manipulator_groups[i - 1].to_bezier(&self.manipulator_groups[i]); - let second_bezier = self.manipulator_groups[i].to_bezier(&self.manipulator_groups[i + 1]); - if first_bezier.handle_end().is_none() || second_bezier.handle_end().is_none() { - continue; - } - let end_tangent = first_bezier.non_normalized_tangent(1.); - let start_tangent = second_bezier.non_normalized_tangent(0.); - - // Compute an average unit vector, weighing the segments by a rough estimation of their relative size. - let segment1_len = first_bezier.length(None); - let segment2_len = second_bezier.length(None); - let average_unit_tangent = (end_tangent.normalize() * segment1_len + start_tangent.normalize() * segment2_len) / (segment1_len + segment2_len); - - // Adjust start and end handles to fit the average tangent - let end_point = first_bezier.end(); - self.manipulator_groups[i].in_handle = Some((average_unit_tangent / 3. * -1.) * end_tangent.length() + end_point); - - let start_point = second_bezier.start(); - self.manipulator_groups[i].out_handle = Some((average_unit_tangent / 3.) * start_tangent.length() + start_point); - } - } - - // TODO: If a segment curls back on itself tightly enough it could intersect again at the portion that should be trimmed. This could cause the Subpaths to be clipped - // at the incorrect location. This can be avoided by first trimming the two Subpaths at any extrema, effectively ignoring loopbacks. - /// Helper function to clip overlap of two intersecting open Subpaths. Returns an optional, as intersections may not exist for certain arrangements and distances. - /// Assumes that the Subpaths represents simple Bezier segments, and clips the Subpaths at the last intersection of the first Subpath, and first intersection of the last Subpath. - pub fn clip_simple_subpaths(subpath1: &Subpath, subpath2: &Subpath) -> Option<(Subpath, Subpath)> { - // Split the first subpath at its last intersection - let intersections1 = subpath1.subpath_intersections(subpath2, None, None); - if intersections1.is_empty() { - return None; - } - let (segment_index, t) = *intersections1.last().unwrap(); - let (clipped_subpath1, _) = subpath1.split(SubpathTValue::Parametric { segment_index, t }); - - // Split the second subpath at its first intersection - let intersections2 = subpath2.subpath_intersections(subpath1, None, None); - if intersections2.is_empty() { - return None; - } - let (segment_index, t) = intersections2[0]; - let (_, clipped_subpath2) = subpath2.split(SubpathTValue::Parametric { segment_index, t }); - - Some((clipped_subpath1, clipped_subpath2.unwrap())) - } - - /// Returns a subpath that results from rotating this subpath around the origin by the given angle (in radians). - /// - pub fn rotate(&self, angle: f64) -> Subpath { - let mut rotated_subpath = self.clone(); - - let affine_transform: DAffine2 = DAffine2::from_angle(angle); - rotated_subpath.apply_transform(affine_transform); - - rotated_subpath - } - - /// Returns a subpath that results from rotating this subpath around the provided point by the given angle (in radians). - pub fn rotate_about_point(&self, angle: f64, pivot: DVec2) -> Subpath { - // Translate before and after the rotation to account for the pivot - let translate: DAffine2 = DAffine2::from_translation(pivot); - let rotate: DAffine2 = DAffine2::from_angle(angle); - let translate_inverse = translate.inverse(); - - let mut rotated_subpath = self.clone(); - rotated_subpath.apply_transform(translate * rotate * translate_inverse); - rotated_subpath - } - - /// Reduces the segments of the subpath into simple subcurves, then scales each subcurve a set `distance` away. - /// The intersections of segments of the subpath are joined using the method specified by the `join` argument. - /// - pub fn offset(&self, distance: f64, join: Join) -> Subpath { - // An offset at a distance 0 from the curve is simply the same curve - // An offset of a single point is not defined - if distance == 0. || self.len() <= 1 || self.len_segments() < 1 { - return self.clone(); - } - - let mut subpaths = self - .iter() - .filter(|bezier| !bezier.is_point()) - .map(|bezier| bezier.offset(distance)) - .filter(|subpath| subpath.len() >= 2) // In some cases the reduced and scaled bézier is marked by is_point (so the subpath is empty). - .collect::>>(); - - let mut drop_common_point = vec![true; self.len()]; - - // Clip or join consecutive Subpaths - for i in 0..subpaths.len() - 1 { - let j = i + 1; - let subpath1 = &subpaths[i]; - let subpath2 = &subpaths[j]; - - let last_segment = subpath1.get_segment(subpath1.len_segments() - 1).unwrap(); - let first_segment = subpath2.get_segment(0).unwrap(); - - // If the anchors are approximately equal, there is no need to clip / join the segments - if last_segment.end().abs_diff_eq(first_segment.start(), MAX_ABSOLUTE_DIFFERENCE) { - continue; - } - - // Calculate the angle formed between two consecutive Subpaths - let out_tangent = self.get_segment(i).unwrap().tangent(TValue::Parametric(1.)); - let in_tangent = self.get_segment(j).unwrap().tangent(TValue::Parametric(0.)); - let angle = out_tangent.angle_to(in_tangent); - - // The angle is concave. The Subpath overlap and must be clipped - let mut apply_join = true; - if (angle > 0. && distance > 0.) || (angle < 0. && distance < 0.) { - // If the distance is large enough, there may still be no intersections. Also, if the angle is close enough to zero, - // subpath intersections may find no intersections. In this case, the points are likely close enough that we can approximate - // the points as being on top of one another. - if let Some((clipped_subpath1, clipped_subpath2)) = Subpath::clip_simple_subpaths(subpath1, subpath2) { - subpaths[i] = clipped_subpath1; - subpaths[j] = clipped_subpath2; - apply_join = false; - } - } - // The angle is convex. The Subpath must be joined using the specified join type - if apply_join { - drop_common_point[j] = false; - match join { - Join::Bevel => {} - Join::Miter(miter_limit) => { - let miter_manipulator_group = subpaths[i].miter_line_join(&subpaths[j], miter_limit); - if let Some(miter_manipulator_group) = miter_manipulator_group { - subpaths[i].manipulator_groups.push(miter_manipulator_group); - } - } - Join::Round => { - let (out_handle, round_point, in_handle) = subpaths[i].round_line_join(&subpaths[j], self.manipulator_groups[j].anchor); - let last_index = subpaths[i].manipulator_groups.len() - 1; - subpaths[i].manipulator_groups[last_index].out_handle = Some(out_handle); - subpaths[i].manipulator_groups.push(round_point.clone()); - subpaths[j].manipulator_groups[0].in_handle = Some(in_handle); - } - } - } - } - - // Clip any overlap in the last segment - if self.closed { - let out_tangent = self.get_segment(self.len_segments() - 1).unwrap().tangent(TValue::Parametric(1.)); - let in_tangent = self.get_segment(0).unwrap().tangent(TValue::Parametric(0.)); - let angle = out_tangent.angle_to(in_tangent); - - let mut apply_join = true; - if (angle > 0. && distance > 0.) || (angle < 0. && distance < 0.) { - if let Some((clipped_subpath1, clipped_subpath2)) = Subpath::clip_simple_subpaths(&subpaths[subpaths.len() - 1], &subpaths[0]) { - // Merge the clipped subpaths - let last_index = subpaths.len() - 1; - subpaths[last_index] = clipped_subpath1; - subpaths[0] = clipped_subpath2; - apply_join = false; - } - } - if apply_join { - drop_common_point[0] = false; - match join { - Join::Bevel => {} - Join::Miter(miter_limit) => { - let last_subpath_index = subpaths.len() - 1; - let miter_manipulator_group = subpaths[last_subpath_index].miter_line_join(&subpaths[0], miter_limit); - if let Some(miter_manipulator_group) = miter_manipulator_group { - subpaths[last_subpath_index].manipulator_groups.push(miter_manipulator_group); - } - } - Join::Round => { - let last_subpath_index = subpaths.len() - 1; - let (out_handle, round_point, in_handle) = subpaths[last_subpath_index].round_line_join(&subpaths[0], self.manipulator_groups[0].anchor); - let last_index = subpaths[last_subpath_index].manipulator_groups.len() - 1; - subpaths[last_subpath_index].manipulator_groups[last_index].out_handle = Some(out_handle); - subpaths[last_subpath_index].manipulator_groups.push(round_point); - subpaths[0].manipulator_groups[0].in_handle = Some(in_handle); - } - } - } - } - - // Merge the subpaths. Drop points which overlap with one another. - let mut manipulator_groups = subpaths[0].manipulator_groups.clone(); - for i in 1..subpaths.len() { - if drop_common_point[i] { - let last_group = manipulator_groups.pop().unwrap(); - let mut manipulators_copy = subpaths[i].manipulator_groups.clone(); - manipulators_copy[0].in_handle = last_group.in_handle; - - manipulator_groups.append(&mut manipulators_copy); - } else { - manipulator_groups.append(&mut subpaths[i].manipulator_groups.clone()); - } - } - if self.closed && drop_common_point[0] { - let last_group = manipulator_groups.pop().unwrap(); - manipulator_groups[0].in_handle = last_group.in_handle; - } - - Subpath::new(manipulator_groups, self.closed) - } - - /// Helper function to combine the two offsets that make up an outline. - pub(crate) fn combine_outline(&self, other: &Subpath, cap: Cap) -> Subpath { - let mut result_manipulator_groups: Vec> = vec![]; - result_manipulator_groups.extend_from_slice(self.manipulator_groups()); - match cap { - Cap::Butt => { - result_manipulator_groups.extend_from_slice(other.manipulator_groups()); - } - Cap::Round => { - let last_index = result_manipulator_groups.len() - 1; - let (out_handle, round_point, in_handle) = self.round_cap(other); - result_manipulator_groups[last_index].out_handle = Some(out_handle); - result_manipulator_groups.push(round_point); - result_manipulator_groups.extend_from_slice(&other.manipulator_groups); - result_manipulator_groups[last_index + 2].in_handle = Some(in_handle); - - let last_index = result_manipulator_groups.len() - 1; - let (out_handle, round_point, in_handle) = other.round_cap(self); - result_manipulator_groups[last_index].out_handle = Some(out_handle); - result_manipulator_groups.push(round_point); - result_manipulator_groups[0].in_handle = Some(in_handle); - } - Cap::Square => { - let square_points = self.square_cap(other); - result_manipulator_groups.extend_from_slice(&square_points); - result_manipulator_groups.extend_from_slice(other.manipulator_groups()); - let square_points = other.square_cap(self); - result_manipulator_groups.extend_from_slice(&square_points); - } - } - Subpath::new(result_manipulator_groups, true) - } - - // TODO: Replace this return type with `Path`, once the `Path` data type has been created. - /// Outline returns a single closed subpath (if the original subpath was open) or two closed subpaths (if the original subpath was closed) that forms - /// an approximate outline around the subpath at a specified distance from the curve. Outline takes the following parameters: - /// - `distance` - The outline's distance from the curve. - /// - `join` - The join type used to cap the endpoints of open bezier curves, and join successive subpath segments. - /// - pub fn outline(&self, distance: f64, join: Join, cap: Cap) -> (Subpath, Option>) { - let is_point = self.is_point(); - let (pos_offset, neg_offset) = if is_point { - let point = self.manipulator_groups[0].anchor; - ( - Subpath::new(vec![ManipulatorGroup::new_anchor(point + DVec2::NEG_Y * distance)], false), - Subpath::new(vec![ManipulatorGroup::new_anchor(point + DVec2::Y * distance)], false), - ) - } else { - (self.offset(distance, join), self.reverse().offset(distance, join)) - }; - - if self.closed && !is_point { - return (pos_offset, Some(neg_offset)); - } - - (pos_offset.combine_outline(&neg_offset, cap), None) - } -} - -#[cfg(test)] -mod tests { - use super::{Cap, Join, ManipulatorGroup, Subpath}; - use crate::EmptyId; - use crate::compare::{compare_points, compare_subpaths, compare_vec_of_points}; - use crate::consts::MAX_ABSOLUTE_DIFFERENCE; - use crate::utils::{SubpathTValue, TValue}; - use glam::DVec2; - - fn set_up_open_subpath() -> Subpath { - let start = DVec2::new(20., 30.); - let middle1 = DVec2::new(80., 90.); - let middle2 = DVec2::new(100., 100.); - let end = DVec2::new(60., 45.); - - let handle1 = DVec2::new(75., 85.); - let handle2 = DVec2::new(40., 30.); - let handle3 = DVec2::new(10., 10.); - - Subpath::new( - vec![ - ManipulatorGroup { - anchor: start, - in_handle: None, - out_handle: Some(handle1), - id: EmptyId, - }, - ManipulatorGroup { - anchor: middle1, - in_handle: None, - out_handle: Some(handle2), - id: EmptyId, - }, - ManipulatorGroup { - anchor: middle2, - in_handle: None, - out_handle: None, - id: EmptyId, - }, - ManipulatorGroup { - anchor: end, - in_handle: None, - out_handle: Some(handle3), - id: EmptyId, - }, - ], - false, - ) - } - - fn set_up_closed_subpath() -> Subpath { - let mut subpath = set_up_open_subpath(); - subpath.closed = true; - subpath - } - - #[test] - fn outline_with_single_point_segment() { - let subpath = Subpath::new( - vec![ - ManipulatorGroup { - anchor: DVec2::new(20., 20.), - out_handle: Some(DVec2::new(10., 90.)), - in_handle: None, - id: EmptyId, - }, - ManipulatorGroup { - anchor: DVec2::new(150., 40.), - out_handle: None, - in_handle: Some(DVec2::new(60., 40.)), - id: EmptyId, - }, - ManipulatorGroup { - anchor: DVec2::new(150., 40.), - out_handle: Some(DVec2::new(40., 120.)), - in_handle: None, - id: EmptyId, - }, - ManipulatorGroup { - anchor: DVec2::new(100., 100.), - out_handle: None, - in_handle: None, - id: EmptyId, - }, - ], - false, - ); - - let outline = subpath.outline(10., crate::Join::Round, crate::Cap::Round).0; - assert!(outline.manipulator_groups.windows(2).all(|pair| !pair[0].anchor.abs_diff_eq(pair[1].anchor, MAX_ABSOLUTE_DIFFERENCE))); - assert!(outline.closed()); - } - - #[test] - /// Even though the bézier here is not marked as a point, the offset and scaled version is. - fn outline_with_point_offset() { - let subpath = Subpath::new( - vec![ - ManipulatorGroup { - anchor: DVec2::new(1122.6253015182049, 610.9441551227939), - out_handle: Some(DVec2::new(1122.6253015182049, 610.9445412168651)), - in_handle: None, - id: EmptyId, - }, - ManipulatorGroup { - anchor: DVec2::new(1122.6258671405062, 610.9453107605276), - out_handle: None, - in_handle: Some(DVec2::new(1122.6254904904154, 610.9449255479497)), - id: EmptyId, - }, - ManipulatorGroup { - anchor: DVec2::new(0., 0.), - out_handle: None, - in_handle: None, - id: EmptyId, - }, - ], - false, - ); - let outline = subpath.outline(4.4, crate::Join::Round, crate::Cap::Round).0; - assert!(outline.manipulator_groups.windows(2).all(|pair| !pair[0].anchor.abs_diff_eq(pair[1].anchor, MAX_ABSOLUTE_DIFFERENCE))); - assert!(outline.closed()); - } - - #[test] - fn split_an_open_subpath() { - let subpath = set_up_open_subpath(); - let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); - let split_pair = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 3.) % 1.)); - let (first, second) = subpath.split(SubpathTValue::GlobalParametric(0.2)); - assert!(second.is_some()); - let second = second.unwrap(); - assert_eq!(first.manipulator_groups[1].anchor, location); - assert_eq!(second.manipulator_groups[0].anchor, location); - assert_eq!(split_pair[0], first.iter().last().unwrap()); - assert_eq!(split_pair[1], second.iter().next().unwrap()); - } - - #[test] - fn split_at_start_of_an_open_subpath() { - let subpath = set_up_open_subpath(); - let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); - let split_pair = subpath.iter().next().unwrap().split(TValue::Parametric(0.)); - let (first, second) = subpath.split(SubpathTValue::GlobalParametric(0.)); - assert!(second.is_some()); - let second = second.unwrap(); - assert_eq!( - first.manipulator_groups[0], - ManipulatorGroup { - anchor: location, - in_handle: None, - out_handle: None, - id: EmptyId, - } - ); - assert_eq!(first.manipulator_groups.len(), 1); - assert_eq!(second.manipulator_groups[0].anchor, location); - assert_eq!(split_pair[1], second.iter().next().unwrap()); - } - - #[test] - fn split_at_end_of_an_open_subpath() { - let subpath = set_up_open_subpath(); - let location = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); - let split_pair = subpath.iter().last().unwrap().split(TValue::Parametric(1.)); - let (first, second) = subpath.split(SubpathTValue::GlobalParametric(1.)); - assert!(second.is_some()); - let second = second.unwrap(); - assert_eq!(first.manipulator_groups[3].anchor, location); - assert_eq!(split_pair[0], first.iter().last().unwrap()); - assert_eq!( - second.manipulator_groups[0], - ManipulatorGroup { - anchor: location, - in_handle: None, - out_handle: None, - id: EmptyId, - } - ); - assert_eq!(second.manipulator_groups.len(), 1); - } - - #[test] - fn split_a_closed_subpath() { - let subpath = set_up_closed_subpath(); - let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); - let split_pair = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 4.) % 1.)); - let (first, second) = subpath.split(SubpathTValue::GlobalParametric(0.2)); - assert!(second.is_none()); - assert_eq!(first.manipulator_groups[0].anchor, location); - assert_eq!(first.manipulator_groups[5].anchor, location); - assert_eq!(first.manipulator_groups.len(), 6); - assert_eq!(split_pair[0], first.iter().last().unwrap()); - assert_eq!(split_pair[1], first.iter().next().unwrap()); - } - - #[test] - fn split_at_start_of_a_closed_subpath() { - let subpath = set_up_closed_subpath(); - let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); - let (first, second) = subpath.split(SubpathTValue::GlobalParametric(0.)); - assert!(second.is_none()); - assert_eq!(first.manipulator_groups[0].anchor, location); - assert_eq!(first.manipulator_groups[4].anchor, location); - assert_eq!(subpath.manipulator_groups[0..], first.manipulator_groups[..4]); - assert!(!first.closed); - assert_eq!(first.iter().last().unwrap(), subpath.iter().last().unwrap()); - assert_eq!(first.iter().next().unwrap(), subpath.iter().next().unwrap()); - } - - #[test] - fn split_at_end_of_a_closed_subpath() { - let subpath = set_up_closed_subpath(); - let location = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); - let (first, second) = subpath.split(SubpathTValue::GlobalParametric(1.)); - assert!(second.is_none()); - assert_eq!(first.manipulator_groups[0].anchor, location); - assert_eq!(first.manipulator_groups[4].anchor, location); - assert_eq!(subpath.manipulator_groups[0..], first.manipulator_groups[..4]); - assert!(!first.closed); - assert_eq!(first.iter().last().unwrap(), subpath.iter().last().unwrap()); - assert_eq!(first.iter().next().unwrap(), subpath.iter().next().unwrap()); - } - - #[test] - fn reverse_an_open_subpath() { - let subpath = set_up_open_subpath(); - let temporary = subpath.reverse(); - let result = temporary.reverse(); - let end = result.len(); - - assert_eq!(temporary.manipulator_groups[0].anchor, result.manipulator_groups[end - 1].anchor); - assert_eq!(temporary.manipulator_groups[0].out_handle, result.manipulator_groups[end - 1].in_handle); - assert_eq!(subpath, result); - } - - #[test] - fn reverse_a_closed_subpath() { - let subpath = set_up_closed_subpath(); - let temporary = subpath.reverse(); - let result = temporary.reverse(); - let end = result.len(); - - // Second manipulator group on the temporary subpath should be the reflected version of the last in the result - assert_eq!(temporary.manipulator_groups[1].anchor, result.manipulator_groups[end - 1].anchor); - assert_eq!(temporary.manipulator_groups[1].in_handle, result.manipulator_groups[end - 1].out_handle); - assert_eq!(temporary.manipulator_groups[1].out_handle, result.manipulator_groups[end - 1].in_handle); - - // The first manipulator group in both should be the reflected versions of each other - assert_eq!(temporary.manipulator_groups[0].anchor, result.manipulator_groups[0].anchor); - assert_eq!(temporary.manipulator_groups[0].in_handle, result.manipulator_groups[0].out_handle); - assert_eq!(temporary.manipulator_groups[0].out_handle, result.manipulator_groups[0].in_handle); - assert_eq!(subpath, result); - } - - #[test] - fn trim_an_open_subpath() { - let subpath = set_up_open_subpath(); - let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); - let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.8)); - let [_, trim_front] = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 3.) % 1.)); - let [trim_back, _] = subpath.iter().last().unwrap().split(TValue::Parametric((0.8 * 3.) % 1.)); - let result = subpath.trim(SubpathTValue::GlobalParametric(0.2), SubpathTValue::GlobalParametric(0.8)); - - assert_eq!(result.manipulator_groups[0].anchor, location_front); - assert_eq!(result.manipulator_groups[3].anchor, location_back); - assert_eq!(trim_front, result.iter().next().unwrap()); - assert_eq!(trim_back, result.iter().last().unwrap()); - } - - #[test] - fn trim_within_a_bezier() { - let subpath = set_up_open_subpath(); - let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.1)); - let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); - let trimmed = subpath.iter().next().unwrap().trim(TValue::Parametric((0.1 * 3.) % 1.), TValue::Parametric((0.2 * 3.) % 1.)); - let result = subpath.trim(SubpathTValue::GlobalParametric(0.1), SubpathTValue::GlobalParametric(0.2)); - assert!(compare_points(result.manipulator_groups[0].anchor, location_front)); - assert!(compare_points(result.manipulator_groups[1].anchor, location_back)); - assert_eq!(trimmed, result.iter().next().unwrap()); - assert_eq!(result.len(), 2); - } - - #[test] - fn trim_first_segment_of_an_open_subpath() { - let subpath = set_up_closed_subpath(); - let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); - let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.25)); - let trimmed = subpath.iter().next().unwrap().trim(TValue::Parametric(0.), TValue::Parametric(1.)); - let result = subpath.trim(SubpathTValue::GlobalParametric(0.), SubpathTValue::GlobalParametric(0.25)); - - assert_eq!(result.manipulator_groups[0].anchor, location_front); - assert_eq!(result.manipulator_groups[1].anchor, location_back); - assert_eq!(trimmed, result.iter().next().unwrap()); - } - - #[test] - fn trim_second_segment_of_an_open_subpath() { - let subpath = set_up_closed_subpath(); - let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.25)); - let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.5)); - let trimmed = subpath.iter().nth(1).unwrap().trim(TValue::Parametric(0.), TValue::Parametric(1.)); - let result = subpath.trim(SubpathTValue::GlobalParametric(0.25), SubpathTValue::GlobalParametric(0.5)); - - assert_eq!(result.manipulator_groups[0].anchor, location_front); - assert_eq!(result.manipulator_groups[1].anchor, location_back); - assert_eq!(trimmed, result.iter().next().unwrap()); - } - - #[test] - fn trim_reverse_in_open_subpath() { - let subpath = set_up_open_subpath(); - let result1 = subpath.trim(SubpathTValue::GlobalParametric(0.8), SubpathTValue::GlobalParametric(0.2)); - let result2 = subpath.trim(SubpathTValue::GlobalParametric(0.2), SubpathTValue::GlobalParametric(0.8)); - - assert!(compare_subpaths::(&result1, &result2)); - } - - #[test] - fn trim_reverse_within_a_bezier() { - let subpath = set_up_open_subpath(); - let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.1)); - let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); - let trimmed = subpath.iter().next().unwrap().trim(TValue::Parametric((0.2 * 3.) % 1.), TValue::Parametric((0.1 * 3.) % 1.)); - let result = subpath.trim(SubpathTValue::GlobalParametric(0.2), SubpathTValue::GlobalParametric(0.1)); - - assert!(compare_points(result.manipulator_groups[0].anchor, location_front)); - assert!(compare_points(result.manipulator_groups[1].anchor, location_back)); - assert!(compare_vec_of_points( - trimmed.get_points().collect(), - result.iter().next().unwrap().get_points().collect(), - MAX_ABSOLUTE_DIFFERENCE - )); - assert_eq!(result.len(), 2); - } - - #[test] - fn trim_a_duplicate_subpath() { - let subpath = set_up_open_subpath(); - let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); - let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); - let result = subpath.trim(SubpathTValue::GlobalParametric(0.), SubpathTValue::GlobalParametric(1.)); - - // Assume that resulting subpath would no longer have the any meaningless handles - let mut expected_subpath = subpath; - expected_subpath[3].out_handle = None; - - assert_eq!(result.manipulator_groups[0].anchor, location_front); - assert!(compare_points(result.manipulator_groups[3].anchor, location_back)); - assert_eq!(expected_subpath, result); - } - - #[test] - fn trim_a_reversed_duplicate_subpath() { - let subpath = set_up_open_subpath(); - let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); - let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); - let result = subpath.trim(SubpathTValue::GlobalParametric(1.), SubpathTValue::GlobalParametric(0.)); - - assert_eq!(result.manipulator_groups[0].anchor, location_front); - assert_eq!(result.manipulator_groups[3].anchor, location_back); - assert!(compare_subpaths::(&subpath, &result)); - } - - #[test] - fn trim_to_end_of_subpath() { - let subpath = set_up_open_subpath(); - let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.8)); - let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); - let trimmed = subpath.iter().last().unwrap().trim(TValue::Parametric((0.8 * 3.) % 1.), TValue::Parametric(1.)); - let result = subpath.trim(SubpathTValue::GlobalParametric(0.8), SubpathTValue::GlobalParametric(1.)); - - assert_eq!(result.manipulator_groups[0].anchor, location_front); - assert!(compare_points(result.manipulator_groups[1].anchor, location_back)); - assert_eq!(trimmed, result.iter().next().unwrap()); - } - - #[test] - fn trim_reversed_to_end_of_subpath() { - let subpath = set_up_open_subpath(); - let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); - let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); - let trimmed = subpath.iter().next().unwrap().trim(TValue::Parametric((0.2 * 3.) % 1.), TValue::Parametric(0.)); - let result = subpath.trim(SubpathTValue::GlobalParametric(0.2), SubpathTValue::GlobalParametric(0.)); - - assert!(compare_points(result.manipulator_groups[0].anchor, location_front)); - assert!(compare_points(result.manipulator_groups[1].anchor, location_back)); - assert!(compare_vec_of_points( - trimmed.get_points().collect(), - result.iter().next().unwrap().get_points().collect(), - MAX_ABSOLUTE_DIFFERENCE - )); - } - - #[test] - fn trim_start_point() { - let subpath = set_up_open_subpath(); - let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); - let result = subpath.trim(SubpathTValue::GlobalParametric(0.), SubpathTValue::GlobalParametric(0.)); - - assert!(compare_points(result.manipulator_groups[0].anchor, location)); - assert!(result.manipulator_groups[0].in_handle.is_none()); - assert!(result.manipulator_groups[0].out_handle.is_none()); - assert_eq!(result.len(), 1); - } - - #[test] - fn trim_middle_point() { - let subpath = set_up_closed_subpath(); - let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.25)); - let result = subpath.trim(SubpathTValue::GlobalParametric(0.25), SubpathTValue::GlobalParametric(0.25)); - - assert!(compare_points(result.manipulator_groups[0].anchor, location)); - assert!(result.manipulator_groups[0].in_handle.is_none()); - assert!(result.manipulator_groups[0].out_handle.is_none()); - assert_eq!(result.len(), 1); - } - - #[test] - fn trim_a_closed_subpath() { - let subpath = set_up_closed_subpath(); - let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); - let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.8)); - let [_, trim_front] = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 4.) % 1.)); - let [trim_back, _] = subpath.iter().last().unwrap().split(TValue::Parametric((0.8 * 4.) % 1.)); - let result = subpath.trim(SubpathTValue::GlobalParametric(0.2), SubpathTValue::GlobalParametric(0.8)); - - assert_eq!(result.manipulator_groups[0].anchor, location_front); - assert_eq!(result.manipulator_groups[4].anchor, location_back); - assert_eq!(trim_front, result.iter().next().unwrap()); - assert_eq!(trim_back, result.iter().last().unwrap()); - } - - #[test] - fn trim_to_end_of_closed_subpath() { - let subpath = set_up_closed_subpath(); - let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.8)); - let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); - let trimmed = subpath.iter().last().unwrap().trim(TValue::Parametric((0.8 * 4.) % 1.), TValue::Parametric(1.)); - let result = subpath.trim(SubpathTValue::GlobalParametric(0.8), SubpathTValue::GlobalParametric(1.)); - - assert_eq!(result.manipulator_groups[0].anchor, location_front); - assert!(compare_points(result.manipulator_groups[1].anchor, location_back)); - assert_eq!(trimmed, result.iter().next().unwrap()); - } - - #[test] - fn trim_across_break_in_a_closed_subpath() { - let subpath = set_up_closed_subpath(); - let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.8)); - let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); - let [_, trim_front] = subpath.iter().last().unwrap().split(TValue::Parametric((0.8 * 4.) % 1.)); - let [trim_back, _] = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 4.) % 1.)); - let result = subpath.trim(SubpathTValue::GlobalParametric(0.8), SubpathTValue::GlobalParametric(0.2)); - - assert_eq!(result.manipulator_groups[0].anchor, location_front); - assert_eq!(result.manipulator_groups[2].anchor, location_back); - assert_eq!(trim_front, result.iter().next().unwrap()); - assert_eq!(trim_back, result.iter().last().unwrap()); - } - - #[test] - fn trim_across_break_in_a_closed_subpath_where_result_is_multiple_segments() { - let subpath = set_up_closed_subpath(); - let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.6)); - let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.4)); - let [_, trim_front] = subpath.iter().nth(2).unwrap().split(TValue::Parametric((0.6 * 4.) % 1.)); - let [trim_back, _] = subpath.iter().nth(1).unwrap().split(TValue::Parametric((0.4 * 4.) % 1.)); - let result = subpath.trim(SubpathTValue::GlobalParametric(0.6), SubpathTValue::GlobalParametric(0.4)); - - assert_eq!(result.manipulator_groups[0].anchor, location_front); - assert_eq!(result.manipulator_groups[4].anchor, location_back); - assert_eq!(trim_front, result.iter().next().unwrap()); - assert_eq!(trim_back, result.iter().last().unwrap()); - } - - #[test] - fn trim_across_break_in_a_closed_subpath_where_ends_are_in_same_segment() { - let subpath = set_up_closed_subpath(); - let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.45)); - let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.4)); - let [_, trim_front] = subpath.iter().nth(1).unwrap().split(TValue::Parametric((0.45 * 4.) % 1.)); - let [trim_back, _] = subpath.iter().nth(1).unwrap().split(TValue::Parametric((0.4 * 4.) % 1.)); - let result = subpath.trim(SubpathTValue::GlobalParametric(0.45), SubpathTValue::GlobalParametric(0.4)); - - assert_eq!(result.manipulator_groups[0].anchor, location_front); - assert_eq!(result.manipulator_groups[5].anchor, location_back); - assert_eq!(trim_front, result.iter().next().unwrap()); - assert_eq!(trim_back, result.iter().last().unwrap()); - } - - #[test] - fn trim_at_break_in_closed_subpath_where_end_is_0() { - let subpath = set_up_closed_subpath(); - let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.8)); - let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); - let trimmed = subpath.iter().last().unwrap().trim(TValue::Parametric((0.8 * 4.) % 1.), TValue::Parametric(1.)); - let result = subpath.trim(SubpathTValue::GlobalParametric(0.8), SubpathTValue::GlobalParametric(0.)); - - assert_eq!(result.manipulator_groups[0].anchor, location_front); - assert_eq!(result.manipulator_groups[1].anchor, location_back); - assert_eq!(trimmed, result.iter().next().unwrap()); - } - - #[test] - fn trim_at_break_in_closed_subpath_where_start_is_1() { - let subpath = set_up_closed_subpath(); - let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); - let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); - let trimmed = subpath.iter().next().unwrap().trim(TValue::Parametric(0.), TValue::Parametric((0.2 * 4.) % 1.)); - let result = subpath.trim(SubpathTValue::GlobalParametric(1.), SubpathTValue::GlobalParametric(0.2)); - - assert_eq!(result.manipulator_groups[0].anchor, location_front); - assert_eq!(result.manipulator_groups[1].anchor, location_back); - assert_eq!(trimmed, result.iter().next().unwrap()); - } - - #[test] - fn trim_at_break_in_closed_subpath_from_1_to_0() { - let subpath = set_up_closed_subpath(); - let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); - let result = subpath.trim(SubpathTValue::GlobalParametric(1.), SubpathTValue::GlobalParametric(0.)); - - assert_eq!(result.manipulator_groups[0].anchor, location); - assert!(result.manipulator_groups[0].in_handle.is_none()); - assert!(result.manipulator_groups[0].out_handle.is_none()); - assert_eq!(result.manipulator_groups.len(), 1); - } - - #[test] - fn outline_single_point_circle() { - let ellipse: Subpath = Subpath::new_ellipse(DVec2::new(0., 0.), DVec2::new(50., 50.)).reverse(); - let p = DVec2::new(25., 25.); - - let subpath: Subpath = Subpath::from_anchors([p, p, p], false); - let outline_open = subpath.outline(25., Join::Bevel, Cap::Round); - assert_eq!(outline_open.0, ellipse); - assert_eq!(outline_open.1, None); - - let subpath_closed: Subpath = Subpath::from_anchors([p, p, p], true); - let outline_closed = subpath_closed.outline(25., Join::Bevel, Cap::Round); - assert_eq!(outline_closed.0, ellipse); - assert_eq!(outline_closed.1, None); - } - - #[test] - fn outline_single_point_square() { - let square: Subpath = Subpath::from_anchors( - [ - DVec2::new(25., 0.), - DVec2::new(0., 0.), - DVec2::new(0., 50.), - DVec2::new(25., 50.), - DVec2::new(50., 50.), - DVec2::new(50., 0.), - ], - true, - ); - let p = DVec2::new(25., 25.); - - let subpath: Subpath = Subpath::from_anchors([p, p, p], false); - let outline_open = subpath.outline(25., Join::Bevel, Cap::Square); - assert_eq!(outline_open.0, square); - assert_eq!(outline_open.1, None); - - let subpath_closed: Subpath = Subpath::from_anchors([p, p, p], true); - let outline_closed = subpath_closed.outline(25., Join::Bevel, Cap::Square); - assert_eq!(outline_closed.0, square); - assert_eq!(outline_closed.1, None); - } -} diff --git a/libraries/bezier-rs/src/utils.rs b/libraries/bezier-rs/src/utils.rs deleted file mode 100644 index 2e80b80e8e..0000000000 --- a/libraries/bezier-rs/src/utils.rs +++ /dev/null @@ -1,423 +0,0 @@ -use crate::consts::{MAX_ABSOLUTE_DIFFERENCE, STRICT_MAX_ABSOLUTE_DIFFERENCE}; -use crate::{ManipulatorGroup, Subpath}; -use glam::{BVec2, DMat2, DVec2}; -use std::fmt::Write; - -#[derive(Copy, Clone, PartialEq)] -/// A structure which can be used to reference a particular point along a `Bezier`. -/// Assuming a 2-dimensional Bezier is represented as a parametric curve defined by components `(x(f(t), y(f(t))))`, this structure defines variants for `f(t)`. -/// - The `Parametric` variant represents the point calculated using the parametric equation of the curve at argument `t`. That is, `f(t) = t`. Speed along the curve's parametric form is not constant. `t` must lie in the range `[0, 1]`. -/// - The `Euclidean` variant represents the point calculated at a distance ratio `t` along the arc length of the curve in the range `[0, 1]`. Speed is constant along the curve's arc length. -/// - E.g. If `d` is the distance from the start point of a `Bezier` to a certain point along the curve, and `l` is the total arc length of the curve, that certain point lies at a distance ratio `t = d / l`. -/// - All `Bezier` functions will implicitly convert a Euclidean [TValue] argument to a parametric `t`-value using binary search, computed within a particular error. That is, a point at distance ratio `t*`, -/// satisfying `|t* - t| <= error`. The default error is `0.001`. Given this requires a lengthier calculation, it is not recommended to use the `Euclidean` or `EuclideanWithinError` variants frequently in computationally intensive tasks. -/// - The `EuclideanWithinError` variant functions exactly as the `Euclidean` variant, but allows the `error` to be customized when computing `t` internally. -pub enum TValue { - Parametric(f64), - Euclidean(f64), - EuclideanWithinError { t: f64, error: f64 }, -} - -#[derive(Copy, Clone, PartialEq)] -pub enum TValueType { - Parametric, - Euclidean, -} - -#[derive(Copy, Clone, PartialEq)] -pub enum SubpathTValue { - Parametric { segment_index: usize, t: f64 }, - GlobalParametric(f64), - Euclidean { segment_index: usize, t: f64 }, - GlobalEuclidean(f64), - EuclideanWithinError { segment_index: usize, t: f64, error: f64 }, - GlobalEuclideanWithinError { t: f64, error: f64 }, -} - -#[derive(Copy, Clone)] -/// Represents the shape of the join between two segments of a path which meet at an angle. -/// Bevel provides a flat connection, Miter provides a sharp connection, and Round provides a rounded connection. -/// As defined in SVG: . -pub enum Join { - /// The join is a straight line between the end points of the offset path sides from the two connecting segments. - Bevel, - /// Optional f64 is the miter limit, which defaults to 4 if `None` or a value less than 1 is provided. - /// The miter limit is used to prevent highly sharp angles from resulting in excessively long miter joins. - /// If the miter limit is exceeded, the join will be converted to a bevel join. - /// The value is the ratio of the miter length to the stroke width. - /// When that ratio is greater than the miter limit, a bevel join is used instead. - Miter(Option), - /// The join is a circular arc between the end points of the offset path sides from the two connecting segments. - Round, -} - -#[derive(Copy, Clone)] -/// Enum to represent the cap type at the ends of an outline -/// As defined in SVG: . -pub enum Cap { - Butt, - Round, - Square, -} - -/// Helper to perform the computation of a and c, where b is the provided point on the curve. -/// Given the correct power of `t` and `(1-t)`, the computation is the same for quadratic and cubic cases. -/// Relevant derivation and the definitions of a, b, and c can be found in [the projection identity section](https://pomax.github.io/bezierinfo/#abc) of Pomax's bezier curve primer. -fn compute_abc_through_points(start_point: DVec2, point_on_curve: DVec2, end_point: DVec2, t_to_nth_power: f64, nth_power_of_one_minus_t: f64) -> [DVec2; 3] { - let point_c_ratio = nth_power_of_one_minus_t / (t_to_nth_power + nth_power_of_one_minus_t); - let c = point_c_ratio * start_point + (1. - point_c_ratio) * end_point; - let ab_bc_ratio = (t_to_nth_power + nth_power_of_one_minus_t - 1.).abs() / (t_to_nth_power + nth_power_of_one_minus_t); - let a = point_on_curve + (point_on_curve - c) / ab_bc_ratio; - [a, point_on_curve, c] -} - -/// Compute `a`, `b`, and `c` for a quadratic curve that fits the start, end and point on curve at `t`. -/// The definition for the `a`, `b`, `c` points are defined in [the projection identity section](https://pomax.github.io/bezierinfo/#abc) of Pomax's bezier curve primer. -pub fn compute_abc_for_quadratic_through_points(start_point: DVec2, point_on_curve: DVec2, end_point: DVec2, t: f64) -> [DVec2; 3] { - let t_squared = t * t; - let one_minus_t = 1. - t; - let squared_one_minus_t = one_minus_t * one_minus_t; - compute_abc_through_points(start_point, point_on_curve, end_point, t_squared, squared_one_minus_t) -} - -/// Compute `a`, `b`, and `c` for a cubic curve that fits the start, end and point on curve at `t`. -/// The definition for the `a`, `b`, `c` points are defined in [the projection identity section](https://pomax.github.io/bezierinfo/#abc) of Pomax's bezier curve primer. -pub fn compute_abc_for_cubic_through_points(start_point: DVec2, point_on_curve: DVec2, end_point: DVec2, t: f64) -> [DVec2; 3] { - let t_cubed = t * t * t; - let one_minus_t = 1. - t; - let cubed_one_minus_t = one_minus_t * one_minus_t * one_minus_t; - - compute_abc_through_points(start_point, point_on_curve, end_point, t_cubed, cubed_one_minus_t) -} - -/// Find the roots of the linear equation `ax + b`. -pub fn solve_linear(a: f64, b: f64) -> [Option; 3] { - // There exist roots when `a` is not 0 - if a.abs() > MAX_ABSOLUTE_DIFFERENCE { [Some(-b / a), None, None] } else { [None; 3] } -} - -/// Find the roots of the linear equation `ax^2 + bx + c`. -/// Precompute the `discriminant` (`b^2 - 4ac`) and `two_times_a` arguments prior to calling this function for efficiency purposes. -pub fn solve_quadratic(discriminant: f64, two_times_a: f64, b: f64, c: f64) -> [Option; 3] { - let mut roots = [None; 3]; - if two_times_a.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE { - roots = solve_linear(b, c); - } else if discriminant.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE { - roots[0] = Some(-b / (two_times_a)); - } else if discriminant > 0. { - let root_discriminant = discriminant.sqrt(); - roots[0] = Some((-b + root_discriminant) / (two_times_a)); - roots[1] = Some((-b - root_discriminant) / (two_times_a)); - } - roots -} - -// TODO: Use an `impl Iterator` return type instead of a `Vec` -/// Solve a cubic of the form `ax^3 + bx^2 + ct + d`. -pub fn solve_cubic(a: f64, b: f64, c: f64, d: f64) -> [Option; 3] { - if a.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE { - if b.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE { - // If both a and b are approximately 0, treat as a linear problem - solve_linear(c, d) - } else { - // If a is approximately 0, treat as a quadratic problem - let discriminant = c * c - 4. * b * d; - solve_quadratic(discriminant, 2. * b, c, d) - } - } else { - // https://momentsingraphics.de/CubicRoots.html - let d_recip = a.recip(); - const ONETHIRD: f64 = 1. / 3.; - let scaled_c2 = b * (ONETHIRD * d_recip); - let scaled_c1 = c * (ONETHIRD * d_recip); - let scaled_c0 = d * d_recip; - if !(scaled_c0.is_finite() && scaled_c1.is_finite() && scaled_c2.is_finite()) { - // cubic coefficient is zero or nearly so. - return solve_quadratic(c * c - 4. * b * d, 2. * b, c, d); - } - let (c0, c1, c2) = (scaled_c0, scaled_c1, scaled_c2); - // (d0, d1, d2) is called "Delta" in article - let d0 = (-c2).mul_add(c2, c1); - let d1 = (-c1).mul_add(c2, c0); - let d2 = c2 * c0 - c1 * c1; - // d is called "Discriminant" - let d = 4. * d0 * d2 - d1 * d1; - // de is called "Depressed.x", Depressed.y = d0 - let de = (-2. * c2).mul_add(d0, d1); - if d < 0. { - let sq = (-0.25 * d).sqrt(); - let r = -0.5 * de; - let t1 = (r + sq).cbrt() + (r - sq).cbrt(); - [Some(t1 - c2), None, None] - } else if d == 0. { - let t1 = (-d0).sqrt().copysign(de); - [Some(t1 - c2), Some(-2. * t1 - c2).filter(|&a| a != t1 - c2), None] - } else { - let th = d.sqrt().atan2(-de) * ONETHIRD; - // (th_cos, th_sin) is called "CubicRoot" - let (th_sin, th_cos) = th.sin_cos(); - // (r0, r1, r2) is called "Root" - let r0 = th_cos; - let ss3 = th_sin * 3_f64.sqrt(); - let r1 = 0.5 * (-th_cos + ss3); - let r2 = 0.5 * (-th_cos - ss3); - let t = 2. * (-d0).sqrt(); - [Some(t.mul_add(r0, -c2)), Some(t.mul_add(r1, -c2)), Some(t.mul_add(r2, -c2))] - } - } -} - -/// Determines if two rectangles have any overlap. The rectangles are represented by a pair of coordinates that designate the top left and bottom right corners (in a graphical coordinate system). -pub fn do_rectangles_overlap(rectangle1: [DVec2; 2], rectangle2: [DVec2; 2]) -> bool { - let [bottom_left1, top_right1] = rectangle1; - let [bottom_left2, top_right2] = rectangle2; - - top_right1.x >= bottom_left2.x && top_right2.x >= bottom_left1.x && top_right2.y >= bottom_left1.y && top_right1.y >= bottom_left2.y -} - -/// Determines if a point is completely inside a rectangle, which is represented as a pair of coordinates [top-left, bottom-right]. -pub fn is_point_inside_rectangle(rect: [DVec2; 2], point: DVec2) -> bool { - let [top_left, bottom_right] = rect; - point.x > top_left.x && point.x < bottom_right.x && point.y > top_left.y && point.y < bottom_right.y -} - -/// Determines if the inner rectangle is completely inside the outer rectangle. The rectangles are represented as pairs of coordinates [top-left, bottom-right]. -pub fn is_rectangle_inside_other(inner: [DVec2; 2], outer: [DVec2; 2]) -> bool { - is_point_inside_rectangle(outer, inner[0]) && is_point_inside_rectangle(outer, inner[1]) -} - -/// Returns the intersection of two lines. The lines are given by a point on the line and its slope (represented by a vector). -pub fn line_intersection(point1: DVec2, point1_slope_vector: DVec2, point2: DVec2, point2_slope_vector: DVec2) -> DVec2 { - assert!(point1_slope_vector.normalize() != point2_slope_vector.normalize()); - - // Find the intersection when the first line is vertical - if f64_compare(point1_slope_vector.x, 0., MAX_ABSOLUTE_DIFFERENCE) { - let m2 = point2_slope_vector.y / point2_slope_vector.x; - let b2 = point2.y - m2 * point2.x; - DVec2::new(point1.x, point1.x * m2 + b2) - } - // Find the intersection when the second line is vertical - else if f64_compare(point2_slope_vector.x, 0., MAX_ABSOLUTE_DIFFERENCE) { - let m1 = point1_slope_vector.y / point1_slope_vector.x; - let b1 = point1.y - m1 * point1.x; - DVec2::new(point2.x, point2.x * m1 + b1) - } - // Find the intersection where neither line is vertical - else { - let m1 = point1_slope_vector.y / point1_slope_vector.x; - let b1 = point1.y - m1 * point1.x; - let m2 = point2_slope_vector.y / point2_slope_vector.x; - let b2 = point2.y - m2 * point2.x; - let intersection_x = (b2 - b1) / (m1 - m2); - DVec2::new(intersection_x, intersection_x * m1 + b1) - } -} - -/// Check if 3 points are collinear. -pub fn are_points_collinear(p1: DVec2, p2: DVec2, p3: DVec2) -> bool { - let matrix = DMat2::from_cols(p1 - p2, p2 - p3); - f64_compare(matrix.determinant() / 2., 0., MAX_ABSOLUTE_DIFFERENCE) -} - -/// Compute the center of the circle that passes through all three provided points. The provided points cannot be collinear. -pub fn compute_circle_center_from_points(p1: DVec2, p2: DVec2, p3: DVec2) -> Option { - if are_points_collinear(p1, p2, p3) { - return None; - } - - let midpoint_a = p1.lerp(p2, 0.5); - let midpoint_b = p2.lerp(p3, 0.5); - let midpoint_c = p3.lerp(p1, 0.5); - - let tangent_a = (p1 - p2).perp(); - let tangent_b = (p2 - p3).perp(); - let tangent_c = (p3 - p1).perp(); - - let intersect_a_b = line_intersection(midpoint_a, tangent_a, midpoint_b, tangent_b); - let intersect_b_c = line_intersection(midpoint_b, tangent_b, midpoint_c, tangent_c); - let intersect_c_a = line_intersection(midpoint_c, tangent_c, midpoint_a, tangent_a); - - Some((intersect_a_b + intersect_b_c + intersect_c_a) / 3.) -} - -/// Compare two `f64` numbers with a provided max absolute value difference. -pub fn f64_compare(a: f64, b: f64, max_abs_diff: f64) -> bool { - (a - b).abs() < max_abs_diff -} - -/// Determine if an `f64` number is within a given range by using a max absolute value difference comparison. -pub fn f64_approximately_in_range(value: f64, min: f64, max: f64, max_abs_diff: f64) -> bool { - (min..=max).contains(&value) || f64_compare(value, min, max_abs_diff) || f64_compare(value, max, max_abs_diff) -} - -/// Compare the two values in a `DVec2` independently with a provided max absolute value difference. -pub fn dvec2_compare(a: DVec2, b: DVec2, max_abs_diff: f64) -> BVec2 { - BVec2::new((a.x - b.x).abs() < max_abs_diff, (a.y - b.y).abs() < max_abs_diff) -} - -/// Determine if the values in a `DVec2` are within a given range independently by using a max absolute value difference comparison. -pub fn dvec2_approximately_in_range(point: DVec2, min_corner: DVec2, max_corner: DVec2, max_abs_diff: f64) -> BVec2 { - (point.cmpge(min_corner) & point.cmple(max_corner)) | dvec2_compare(point, min_corner, max_abs_diff) | dvec2_compare(point, max_corner, max_abs_diff) -} - -/// Calculate a new position for a point given its original position, a unit vector in the desired direction, and a distance to move it by. -pub fn scale_point_from_direction_vector(point: DVec2, direction_unit_vector: DVec2, should_flip_direction: bool, distance: f64) -> DVec2 { - let should_reverse_factor = if should_flip_direction { -1. } else { 1. }; - point + distance * direction_unit_vector * should_reverse_factor -} - -/// Scale a point by a given distance with respect to the provided origin. -pub fn scale_point_from_origin(point: DVec2, origin: DVec2, should_flip_direction: bool, distance: f64) -> DVec2 { - scale_point_from_direction_vector(point, (origin - point).normalize(), should_flip_direction, distance) -} - -/// Computes the necessary details to form a circular join from `left` to `right`, along a circle around `center`. -/// By default, the angle is assumed to be 180 degrees. -pub fn compute_circular_subpath_details(left: DVec2, arc_point: DVec2, right: DVec2, center: DVec2, angle: Option) -> (DVec2, ManipulatorGroup, DVec2) { - let center_to_arc_point = arc_point - center; - - // Based on https://pomax.github.io/bezierinfo/#circles_cubic - let handle_offset_factor = if let Some(angle) = angle { 4. / 3. * (angle / 4.).tan() } else { 0.551784777779014 }; - - ( - left - (left - center).perp() * handle_offset_factor, - ManipulatorGroup::new( - arc_point, - Some(arc_point + center_to_arc_point.perp() * handle_offset_factor), - Some(arc_point - center_to_arc_point.perp() * handle_offset_factor), - ), - right + (right - center).perp() * handle_offset_factor, - ) -} - -pub fn format_point(svg: &mut String, prefix: &str, x: f64, y: f64) -> std::fmt::Result { - write!(svg, "{prefix}{:.6}", x)?; - let trimmed_length = svg.trim_end_matches('0').trim_end_matches('.').len(); - svg.truncate(trimmed_length); - - write!(svg, ",{:.6}", y)?; - let trimmed_length = svg.trim_end_matches('0').trim_end_matches('.').len(); - svg.truncate(trimmed_length); - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::consts::MAX_ABSOLUTE_DIFFERENCE; - use crate::{Bezier, EmptyId}; - - /// Compare vectors of `f64`s with a provided max absolute value difference. - fn f64_compare_vector(a: Vec, b: Vec, max_abs_diff: f64) -> bool { - a.len() == b.len() && a.into_iter().zip(b).all(|(a, b)| f64_compare(a, b, max_abs_diff)) - } - - fn collect_roots(mut roots: [Option; 3]) -> Vec { - roots.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap()); - roots.into_iter().flatten().collect() - } - - #[test] - fn test_solve_linear() { - // Line that is on the x-axis - assert!(collect_roots(solve_linear(0., 0.)).is_empty()); - // Line that is parallel to but not on the x-axis - assert!(collect_roots(solve_linear(0., 1.)).is_empty()); - // Line with a non-zero slope - assert!(collect_roots(solve_linear(2., -8.)) == vec![4.]); - } - - #[test] - fn test_solve_cubic() { - // discriminant == 0 - let roots1 = collect_roots(solve_cubic(1., 0., 0., 0.)); - assert!(roots1 == vec![0.]); - - let roots2 = collect_roots(solve_cubic(1., 3., 0., -4.)); - assert!(roots2 == vec![-2., 1.]); - - // p == 0 - let roots3 = collect_roots(solve_cubic(1., 0., 0., -1.)); - assert!(roots3 == vec![1.]); - - // discriminant > 0 - let roots4 = collect_roots(solve_cubic(1., 3., 0., 2.)); - assert!(f64_compare_vector(roots4, vec![-3.196], MAX_ABSOLUTE_DIFFERENCE)); - - // discriminant < 0 - let roots5 = collect_roots(solve_cubic(1., 3., 0., -1.)); - assert!(f64_compare_vector(roots5, vec![-2.879, -0.653, 0.532], MAX_ABSOLUTE_DIFFERENCE)); - - // quadratic - let roots6 = collect_roots(solve_cubic(0., 3., 0., -3.)); - assert!(roots6 == vec![-1., 1.]); - - // linear - let roots7 = collect_roots(solve_cubic(0., 0., 1., -1.)); - assert!(roots7 == vec![1.]); - } - - #[test] - fn test_do_rectangles_overlap() { - // Rectangles overlap - assert!(do_rectangles_overlap([DVec2::new(0., 0.), DVec2::new(20., 20.)], [DVec2::new(10., 10.), DVec2::new(30., 20.)])); - // Rectangles share a side - assert!(do_rectangles_overlap([DVec2::new(0., 0.), DVec2::new(10., 10.)], [DVec2::new(10., 10.), DVec2::new(30., 30.)])); - // Rectangle inside the other - assert!(do_rectangles_overlap([DVec2::new(0., 0.), DVec2::new(10., 10.)], [DVec2::new(2., 2.), DVec2::new(6., 4.)])); - // No overlap, rectangles are beside each other - assert!(!do_rectangles_overlap([DVec2::new(0., 0.), DVec2::new(10., 10.)], [DVec2::new(20., 0.), DVec2::new(30., 10.)])); - // No overlap, rectangles are above and below each other - assert!(!do_rectangles_overlap([DVec2::new(0., 0.), DVec2::new(10., 10.)], [DVec2::new(0., 20.), DVec2::new(20., 30.)])); - } - - #[test] - fn test_is_rectangle_inside_other() { - assert!(!is_rectangle_inside_other([DVec2::new(10., 10.), DVec2::new(50., 50.)], [DVec2::new(10., 10.), DVec2::new(50., 50.)])); - assert!(is_rectangle_inside_other( - [DVec2::new(10.01, 10.01), DVec2::new(49., 49.)], - [DVec2::new(10., 10.), DVec2::new(50., 50.)] - )); - assert!(!is_rectangle_inside_other([DVec2::new(5., 5.), DVec2::new(50., 9.99)], [DVec2::new(10., 10.), DVec2::new(50., 50.)])); - } - - #[test] - fn test_find_intersection() { - // y = 2x + 10 - // y = 5x + 4 - // intersect at (2, 14) - - let start1 = DVec2::new(0., 10.); - let end1 = DVec2::new(0., 4.); - let start_direction1 = DVec2::new(1., 2.); - let end_direction1 = DVec2::new(1., 5.); - assert!(line_intersection(start1, start_direction1, end1, end_direction1) == DVec2::new(2., 14.)); - - // y = x - // y = -x + 8 - // intersect at (4, 4) - - let start2 = DVec2::new(0., 0.); - let end2 = DVec2::new(8., 0.); - let start_direction2 = DVec2::new(1., 1.); - let end_direction2 = DVec2::new(1., -1.); - assert!(line_intersection(start2, start_direction2, end2, end_direction2) == DVec2::new(4., 4.)); - } - - #[test] - fn test_are_points_collinear() { - assert!(are_points_collinear(DVec2::new(2., 4.), DVec2::new(6., 8.), DVec2::new(4., 6.))); - assert!(!are_points_collinear(DVec2::new(1., 4.), DVec2::new(6., 8.), DVec2::new(4., 6.))); - } - - #[test] - fn test_compute_circle_center_from_points() { - // 3/4 of unit circle - let center1 = compute_circle_center_from_points(DVec2::new(0., 1.), DVec2::new(-1., 0.), DVec2::new(1., 0.)); - assert_eq!(center1.unwrap(), DVec2::new(0., 0.)); - // 1/4 of unit circle - let center2 = compute_circle_center_from_points(DVec2::new(-1., 0.), DVec2::new(0., 1.), DVec2::new(1., 0.)); - assert_eq!(center2.unwrap(), DVec2::new(0., 0.)); - } -} diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index 2ee93f9a0a..9646d81bde 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -864,7 +864,7 @@ async fn offset_path(_: impl Ctx, content: Table, distance: f64, join: S for mut bezpath in bezpaths { bezpath.apply_affine(transform); - // Taking the existing stroke data and passing it to Bezier-rs to generate new paths. + // Taking the existing stroke data and passing it to Kurbo to generate new paths. let mut bezpath_out = offset_bezpath( &bezpath, -distance, diff --git a/node-graph/gcore/src/vector/vector_types.rs b/node-graph/gcore/src/vector/vector_types.rs index 442d834c10..84b4acd365 100644 --- a/node-graph/gcore/src/vector/vector_types.rs +++ b/node-graph/gcore/src/vector/vector_types.rs @@ -59,7 +59,7 @@ impl std::hash::Hash for Vector { } impl Vector { - /// Add a Bezier-rs subpath to this path. + /// Add a subpath to this vector path. pub fn append_subpath(&mut self, subpath: impl Borrow>, preserve_id: bool) { let subpath: &Subpath = subpath.borrow(); let stroke_id = StrokeId::ZERO; @@ -131,7 +131,7 @@ impl Vector { self.point_domain.push(id, point.position); } - /// Construct some new vector path from a single Bezier-rs subpath with an identity transform and black fill. + /// Construct some new vector path from a single subpath with an identity transform and black fill. pub fn from_subpath(subpath: impl Borrow>) -> Self { Self::from_subpaths([subpath], false) } @@ -143,7 +143,7 @@ impl Vector { vector } - /// Construct some new vector path from Bezier-rs subpaths with an identity transform and black fill. + /// Construct some new vector path from subpaths with an identity transform and black fill. pub fn from_subpaths(subpaths: impl IntoIterator>>, preserve_id: bool) -> Self { let mut vector = Self::default(); diff --git a/node-graph/graster-nodes/src/generate_curves.rs b/node-graph/graster-nodes/src/generate_curves.rs index 6977d84f2d..d4ded79983 100644 --- a/node-graph/graster-nodes/src/generate_curves.rs +++ b/node-graph/graster-nodes/src/generate_curves.rs @@ -1,5 +1,3 @@ -//! requires bezier-rs - use crate::curve::{Curve, CurveManipulatorGroup, ValueMapperNode}; use graphene_core::color::{Channel, Linear}; use graphene_core::context::Ctx; @@ -34,7 +32,7 @@ fn generate_curves(_: impl Ctx, curve: Curve, #[implementat pathseg_find_tvalues_for_x(segment, x) .next() .map(|t| segment.eval(t.clamp(0., 1.)).y) - // Fall back to a very bad approximation if Bezier-rs fails + // Fall back to a very bad approximation if the above fails .unwrap_or_else(|| (x - x0) / (x3 - x0) * (y3 - y0) + y0) }; lut[index] = C::from_f64(y); diff --git a/website/.gitignore b/website/.gitignore index c9dbb6337b..0fc0af5ddb 100644 --- a/website/.gitignore +++ b/website/.gitignore @@ -3,4 +3,3 @@ public/ static/fonts/ static/syntax-highlighting.css static/text-balancer.js -other/editor-structure/replacement.html diff --git a/website/other/editor-structure/generate.js b/website/build-scripts/generate-editor-structure.js similarity index 98% rename from website/other/editor-structure/generate.js rename to website/build-scripts/generate-editor-structure.js index b9ce6d9969..02db735a59 100644 --- a/website/other/editor-structure/generate.js +++ b/website/build-scripts/generate-editor-structure.js @@ -95,7 +95,7 @@ const outputFile = process.argv[3]; if (!inputFile || !outputFile) { console.error("Error: Please provide the input text and output HTML file paths as arguments."); - console.log("Usage: node generate.js "); + console.log("Usage: node generate-editor-structure.js "); process.exit(1); } diff --git a/website/install-fonts.js b/website/build-scripts/install-fonts.js similarity index 92% rename from website/install-fonts.js rename to website/build-scripts/install-fonts.js index e3fbb12d68..4cfa10df58 100644 --- a/website/install-fonts.js +++ b/website/build-scripts/install-fonts.js @@ -8,15 +8,15 @@ const basePath = path.resolve(__dirname); // Define files to copy as [source, destination] pairs // Files with the same destination will be concatenated const FILES_TO_COPY = [ - ["node_modules/@fontsource-variable/inter/opsz.css", "static/fonts/common.css"], - ["node_modules/@fontsource-variable/inter/opsz-italic.css", "static/fonts/common.css"], - ["node_modules/@fontsource/bona-nova/700.css", "static/fonts/common.css"], + ["../node_modules/@fontsource-variable/inter/opsz.css", "../static/fonts/common.css"], + ["../node_modules/@fontsource-variable/inter/opsz-italic.css", "../static/fonts/common.css"], + ["../node_modules/@fontsource/bona-nova/700.css", "../static/fonts/common.css"], ]; // Define directories to copy recursively as [source, destination] pairs const DIRECTORIES_TO_COPY = [ - ["node_modules/@fontsource-variable/inter/files", "static/fonts/files"], - ["node_modules/@fontsource/bona-nova/files", "static/fonts/files"], + ["../node_modules/@fontsource-variable/inter/files", "../static/fonts/files"], + ["../node_modules/@fontsource/bona-nova/files", "../static/fonts/files"], ]; // Track processed destination files and CSS content @@ -159,7 +159,7 @@ console.log("\nFont installation complete!"); // Fetch and save text-balancer.js, which we don't commit to the repo so we're not version controlling dependency code const textBalancerUrl = "https://static.graphite.rs/text-balancer/text-balancer.js"; -const textBalancerDest = path.join(basePath, "static", "text-balancer.js"); +const textBalancerDest = path.join(basePath, "../static", "text-balancer.js"); console.log("\nDownloading text-balancer.js..."); https .get(textBalancerUrl, (res) => { diff --git a/website/content/volunteer/guide/student-projects/_index.md b/website/content/volunteer/guide/student-projects/_index.md index 434cc425cb..00913fc9f1 100644 --- a/website/content/volunteer/guide/student-projects/_index.md +++ b/website/content/volunteer/guide/student-projects/_index.md @@ -335,7 +335,7 @@ Duration: 9 months Students: Hannah Li, Rob Nadal, Thomas Cheng, Linda Zheng, Jackie Chen - [Bezier-rs library](https://crates.io/crates/bezier-rs) -- [Interactive web demo](/libraries/bezier-rs/) +- [Interactive web demo](https://keavon.github.io/Bezier-rs/) **Outcomes:** The student group designed an API for representing and manipulating Bezier curves and paths as a standalone Rust library which was published to crates.io. It now serves as the underlying vector data format used in Graphite, and acts as a testbed for new computational geometry algorithms. The team also built an interactive web demo catalog to showcase many of the algorithms, which are also handily embedded in the library's [documentation](https://docs.rs/bezier-rs/latest/bezier_rs/). diff --git a/website/other/bezier-rs-demos/.eslintrc.cjs b/website/other/bezier-rs-demos/.eslintrc.cjs deleted file mode 100644 index 57e0e66911..0000000000 --- a/website/other/bezier-rs-demos/.eslintrc.cjs +++ /dev/null @@ -1,87 +0,0 @@ -module.exports = { - root: true, - env: { browser: true, node: true }, - extends: ["eslint:recommended", "plugin:import/recommended", "plugin:@typescript-eslint/recommended", "plugin:import/typescript", "prettier"], - plugins: ["import", "@typescript-eslint", "prettier"], - settings: { - "import/parsers": { "@typescript-eslint/parser": [".ts"] }, - "import/resolver": { typescript: true, node: true }, - }, - parser: "@typescript-eslint/parser", - parserOptions: { - ecmaVersion: "latest", - project: "./tsconfig.json", - }, - ignorePatterns: [ - // Ignore generated directories - "node_modules/", - "dist/", - "pkg/", - "wasm/pkg/", - // Don't ignore JS and TS dotfiles in this folder - "!.*.js", - "!.*.ts", - ], - overrides: [ - { - extends: ["plugin:@typescript-eslint/disable-type-checked"], - files: ["./*.js", "./*.cjs"], - }, - ], - rules: { - // Standard ESLint config (for ordinary JS syntax linting) - indent: "off", - quotes: ["error", "double", { allowTemplateLiterals: true }], - camelcase: ["error", { properties: "always" }], - "linebreak-style": ["error", "unix"], - "eol-last": ["error", "always"], - "max-len": ["error", { code: 200, tabWidth: 4, ignorePattern: `d="([\\s\\S]*?)"` }], - "prefer-destructuring": "off", - "no-console": "warn", - "no-debugger": "warn", - "no-param-reassign": ["error", { props: false }], - "no-bitwise": "off", - "no-shadow": "off", - "no-use-before-define": "off", - "no-restricted-imports": ["error", { patterns: [".*", "!@/*"] }], - - // TypeScript plugin config (for TS-specific linting) - "@typescript-eslint/indent": "off", - "@typescript-eslint/camelcase": "off", - "@typescript-eslint/no-use-before-define": "off", - "@typescript-eslint/no-unused-vars": [ - "error", - { - args: "all", - argsIgnorePattern: "^_", - caughtErrors: "all", - caughtErrorsIgnorePattern: "^_", - destructuredArrayIgnorePattern: "^_", - varsIgnorePattern: "^_", - ignoreRestSiblings: true, - }, - ], - "@typescript-eslint/consistent-type-imports": "error", - "@typescript-eslint/consistent-type-definitions": ["error", "type"], - "@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "as", objectLiteralTypeAssertions: "never" }], - "@typescript-eslint/consistent-indexed-object-style": ["error", "record"], - "@typescript-eslint/consistent-generic-constructors": ["error", "constructor"], - "@typescript-eslint/no-restricted-types": ["error", { types: { null: "Use `undefined` instead." } }], - - // Prettier plugin config (for validating and fixing formatting) - "prettier/prettier": "error", - - // Import plugin config (for intelligently validating module import statements) - "import/no-unresolved": "error", - "import/prefer-default-export": "off", - "import/no-relative-packages": "error", - "import/order": [ - "error", - { - alphabetize: { order: "asc", caseInsensitive: true }, - "newlines-between": "always-and-inside-groups", - warnOnUnassignedImports: true, - }, - ], - }, -}; diff --git a/website/other/bezier-rs-demos/.gitignore b/website/other/bezier-rs-demos/.gitignore deleted file mode 100644 index 7b77d66683..0000000000 --- a/website/other/bezier-rs-demos/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -/node_modules -!/public -/public/build -/dist -/wasm/pkg diff --git a/website/other/bezier-rs-demos/.prettierrc b/website/other/bezier-rs-demos/.prettierrc deleted file mode 100644 index 51f101984c..0000000000 --- a/website/other/bezier-rs-demos/.prettierrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "singleQuote": false, - "useTabs": true, - "tabWidth": 4, - "printWidth": 200 -} diff --git a/website/other/bezier-rs-demos/README.md b/website/other/bezier-rs-demos/README.md deleted file mode 100644 index 638fc53b97..0000000000 --- a/website/other/bezier-rs-demos/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# Bezier-rs interactive documentation - -Open these interactive docs: - -This page also serves isolated demos for iframes used in the Rustdoc [crate documentation](https://docs.rs/bezier-rs/latest/bezier_rs/). - -## Building and running - -Make sure [Node.js](https://nodejs.org/) (the latest LTS version) and [Rust](https://www.rust-lang.org/) (the latest stable release) are installed on your system, and [wasm-pack](https://rustwasm.github.io/wasm-pack/) has been installed by running `cargo install wasm-pack`. - -- To run the development server with hot reloading: - ``` - npm start - ``` -- To compile an optimized production build: - ``` - npm run build - ``` diff --git a/website/other/bezier-rs-demos/index.html b/website/other/bezier-rs-demos/index.html deleted file mode 100644 index d422d29fdf..0000000000 --- a/website/other/bezier-rs-demos/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - Bezier-rs Interactive Documentation - - - - - - - - diff --git a/website/other/bezier-rs-demos/package-installer.js b/website/other/bezier-rs-demos/package-installer.js deleted file mode 100644 index 64970e38a6..0000000000 --- a/website/other/bezier-rs-demos/package-installer.js +++ /dev/null @@ -1,41 +0,0 @@ -// This script automatically installs the npm packages listed in package-lock.json and runs before `npm start`. -// It skips the installation if this has already run and neither package.json nor package-lock.json has been modified since. - -import { execSync } from "child_process"; -import { existsSync, statSync, writeFileSync } from "fs"; - -const INSTALL_TIMESTAMP_FILE = "node_modules/.install-timestamp"; - -// Checks if the install is needed by comparing modification times -const isInstallNeeded = () => { - if (!existsSync(INSTALL_TIMESTAMP_FILE)) return true; - - const timestamp = statSync(INSTALL_TIMESTAMP_FILE).mtime; - return ["package.json", "package-lock.json"].some((file) => { - return existsSync(file) && statSync(file).mtime > timestamp; - }); -}; - -// Run `npm ci` if needed and update the install timestamp -if (isInstallNeeded()) { - try { - // eslint-disable-next-line no-console - console.log("Installing npm packages..."); - - // Check if packages are up to date, doing so quickly by using `npm ci`, preferring local cached packages, and skipping the package audit and other checks - execSync("npm ci --prefer-offline --no-audit --no-fund", { stdio: "inherit" }); - - // Touch the install timestamp file - writeFileSync(INSTALL_TIMESTAMP_FILE, ""); - - // eslint-disable-next-line no-console - console.log("Finished installing npm packages."); - } catch (_) { - // eslint-disable-next-line no-console - console.error("Failed to install npm packages. Please run `npm install` from the `/frontend` directory."); - process.exit(1); - } -} else { - // eslint-disable-next-line no-console - console.log("All npm packages are up-to-date."); -} diff --git a/website/other/bezier-rs-demos/package-lock.json b/website/other/bezier-rs-demos/package-lock.json deleted file mode 100644 index 8aeb4f2b35..0000000000 --- a/website/other/bezier-rs-demos/package-lock.json +++ /dev/null @@ -1,5181 +0,0 @@ -{ - "name": "bezier-rs-demos", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "bezier-rs-demos", - "version": "0.1.0", - "devDependencies": { - "@types/node": "^22.6.1", - "@typescript-eslint/eslint-plugin": "^8.7.0", - "@typescript-eslint/parser": "^8.7.0", - "concurrently": "^9.0.1", - "eslint-config-prettier": "^9.1.0", - "eslint-import-resolver-typescript": "^3.6.3", - "eslint-plugin-import": "^2.30.0", - "eslint-plugin-prettier": "^5.2.1", - "prettier": "^3.3.3", - "ts-node": "^10.9.2", - "typescript": "^5.6.2", - "vite": "^5.4.7" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", - "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.1.0.tgz", - "integrity": "sha512-kLrdPDJE1ckPo94kmPPf9Hfd0DU0Jw6oKYrhe+pwSC0iTUInmTa+w6fw8sGgcfkFJGNdWOUeOaDM4quW4a7OkA==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", - "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", - "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "9.22.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.22.0.tgz", - "integrity": "sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", - "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@eslint/core": "^0.12.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", - "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nolyfill/is-core-module": { - "version": "1.0.39", - "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", - "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.4.0" - } - }, - "node_modules/@pkgr/core": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", - "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.9.tgz", - "integrity": "sha512-qZdlImWXur0CFakn2BJ2znJOdqYZKiedEPEVNTBrpfPjc/YuTGcaYZcdmNFTkUj3DU0ZM/AElcM8Ybww3xVLzA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.9.tgz", - "integrity": "sha512-4KW7P53h6HtJf5Y608T1ISKvNIYLWRKMvfnG0c44M6In4DQVU58HZFEVhWINDZKp7FZps98G3gxwC1sb0wXUUg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.9.tgz", - "integrity": "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.9.tgz", - "integrity": "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.9.tgz", - "integrity": "sha512-2lzjQPJbN5UnHm7bHIUKFMulGTQwdvOkouJDpPysJS+QFBGDJqcfh+CxxtG23Ik/9tEvnebQiylYoazFMAgrYw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.9.tgz", - "integrity": "sha512-SLl0hi2Ah2H7xQYd6Qaiu01kFPzQ+hqvdYSoOtHYg/zCIFs6t8sV95kaoqjzjFwuYQLtOI0RZre/Ke0nPaQV+g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.9.tgz", - "integrity": "sha512-88I+D3TeKItrw+Y/2ud4Tw0+3CxQ2kLgu3QvrogZ0OfkmX/DEppehus7L3TS2Q4lpB+hYyxhkQiYPJ6Mf5/dPg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.9.tgz", - "integrity": "sha512-3qyfWljSFHi9zH0KgtEPG4cBXHDFhwD8kwg6xLfHQ0IWuH9crp005GfoUUh/6w9/FWGBwEHg3lxK1iHRN1MFlA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.9.tgz", - "integrity": "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.9.tgz", - "integrity": "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.9.tgz", - "integrity": "sha512-dRAgTfDsn0TE0HI6cmo13hemKpVHOEyeciGtvlBTkpx/F65kTvShtY/EVyZEIfxFkV5JJTuQ9tP5HGBS0hfxIg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.9.tgz", - "integrity": "sha512-PHcNOAEhkoMSQtMf+rJofwisZqaU8iQ8EaSps58f5HYll9EAY5BSErCZ8qBDMVbq88h4UxaNPlbrKqfWP8RfJA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.9.tgz", - "integrity": "sha512-Z2i0Uy5G96KBYKjeQFKbbsB54xFOL5/y1P5wNBsbXB8yE+At3oh0DVMjQVzCJRJSfReiB2tX8T6HUFZ2k8iaKg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.9.tgz", - "integrity": "sha512-U+5SwTMoeYXoDzJX5dhDTxRltSrIax8KWwfaaYcynuJw8mT33W7oOgz0a+AaXtGuvhzTr2tVKh5UO8GVANTxyQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.9.tgz", - "integrity": "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.9.tgz", - "integrity": "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.9.tgz", - "integrity": "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.9.tgz", - "integrity": "sha512-KB48mPtaoHy1AwDNkAJfHXvHp24H0ryZog28spEs0V48l3H1fr4i37tiyHsgKZJnCmvxsbATdZGBpbmxTE3a9w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz", - "integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.13.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz", - "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.0.tgz", - "integrity": "sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.26.0", - "@typescript-eslint/type-utils": "8.26.0", - "@typescript-eslint/utils": "8.26.0", - "@typescript-eslint/visitor-keys": "8.26.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.26.0.tgz", - "integrity": "sha512-mNtXP9LTVBy14ZF3o7JG69gRPBK/2QWtQd0j0oH26HcY/foyJJau6pNUez7QrM5UHnSvwlQcJXKsk0I99B9pOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.26.0", - "@typescript-eslint/types": "8.26.0", - "@typescript-eslint/typescript-estree": "8.26.0", - "@typescript-eslint/visitor-keys": "8.26.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.26.0.tgz", - "integrity": "sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.26.0", - "@typescript-eslint/visitor-keys": "8.26.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.26.0.tgz", - "integrity": "sha512-ruk0RNChLKz3zKGn2LwXuVoeBcUMh+jaqzN461uMMdxy5H9epZqIBtYj7UiPXRuOpaALXGbmRuZQhmwHhaS04Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "8.26.0", - "@typescript-eslint/utils": "8.26.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.26.0.tgz", - "integrity": "sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.0.tgz", - "integrity": "sha512-tiJ1Hvy/V/oMVRTbEOIeemA2XoylimlDQ03CgPPNaHYZbpsc78Hmngnt+WXZfJX1pjQ711V7g0H7cSJThGYfPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.26.0", - "@typescript-eslint/visitor-keys": "8.26.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.26.0.tgz", - "integrity": "sha512-2L2tU3FVwhvU14LndnQCA2frYC8JnPDVKyQtWFPf8IYFMt/ykEN1bPolNhNbCVgOmdzTlWdusCTKA/9nKrf8Ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.26.0", - "@typescript-eslint/types": "8.26.0", - "@typescript-eslint/typescript-estree": "8.26.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.0.tgz", - "integrity": "sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.26.0", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peer": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0", - "peer": true - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", - "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/concurrently": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz", - "integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2", - "lodash": "^4.17.21", - "rxjs": "^7.8.1", - "shell-quote": "^1.8.1", - "supports-color": "^8.1.1", - "tree-kill": "^1.2.2", - "yargs": "^17.7.2" - }, - "bin": { - "conc": "dist/bin/concurrently.js", - "concurrently": "dist/bin/concurrently.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/enhanced-resolve": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", - "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/es-abstract": { - "version": "1.23.9", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", - "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.0", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-regex": "^1.2.1", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.0", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.3", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.3", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.18" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.22.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.22.0.tgz", - "integrity": "sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.2", - "@eslint/config-helpers": "^0.1.0", - "@eslint/core": "^0.12.0", - "@eslint/eslintrc": "^3.3.0", - "@eslint/js": "9.22.0", - "@eslint/plugin-kit": "^0.2.7", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-import-resolver-typescript": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.8.3.tgz", - "integrity": "sha512-A0bu4Ks2QqDWNpeEgTQMPTngaMhuDu4yv6xpftBMAf+1ziXnpx+eSR1WRfoPTe2BAiAjHFZ7kSNx1fvr5g5pmQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@nolyfill/is-core-module": "1.0.39", - "debug": "^4.3.7", - "enhanced-resolve": "^5.15.0", - "get-tsconfig": "^4.10.0", - "is-bun-module": "^1.0.2", - "stable-hash": "^0.0.4", - "tinyglobby": "^0.2.12" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" - }, - "peerDependencies": { - "eslint": "*", - "eslint-plugin-import": "*", - "eslint-plugin-import-x": "*" - }, - "peerDependenciesMeta": { - "eslint-plugin-import": { - "optional": true - }, - "eslint-plugin-import-x": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", - "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", - "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.8", - "array.prototype.findlastindex": "^1.2.5", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.0", - "hasown": "^2.0.2", - "is-core-module": "^2.15.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "object.groupby": "^1.0.3", - "object.values": "^1.2.0", - "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.8", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-plugin-prettier": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz", - "integrity": "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.9.1" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-plugin-prettier" - }, - "peerDependencies": { - "@types/eslint": ">=8.0.0", - "eslint": ">=8.0.0", - "eslint-config-prettier": "*", - "prettier": ">=3.0.0" - }, - "peerDependenciesMeta": { - "@types/eslint": { - "optional": true - }, - "eslint-config-prettier": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "acorn": "^8.14.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/fast-diff": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC", - "peer": true - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-tsconfig": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", - "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bun-module": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.3.0.tgz", - "integrity": "sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.6.3" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC", - "peer": true - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "license": "ISC" - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.9.tgz", - "integrity": "sha512-SppoicMGpZvbF1l3z4x7No3OlIjP7QJvC9XR7AhZr1kL133KHnKPztkKDc+Ir4aJ/1VhTySrtKhrsycmrMQfvg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.8", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.9.tgz", - "integrity": "sha512-nF5XYqWWp9hx/LrpC8sZvvvmq0TeTjQgaZHYmAgwysT9nh8sWnZhBnM8ZyVbbJFIQBLwHDNoMqsBZBbUo4U8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.6" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.34.9", - "@rollup/rollup-android-arm64": "4.34.9", - "@rollup/rollup-darwin-arm64": "4.34.9", - "@rollup/rollup-darwin-x64": "4.34.9", - "@rollup/rollup-freebsd-arm64": "4.34.9", - "@rollup/rollup-freebsd-x64": "4.34.9", - "@rollup/rollup-linux-arm-gnueabihf": "4.34.9", - "@rollup/rollup-linux-arm-musleabihf": "4.34.9", - "@rollup/rollup-linux-arm64-gnu": "4.34.9", - "@rollup/rollup-linux-arm64-musl": "4.34.9", - "@rollup/rollup-linux-loongarch64-gnu": "4.34.9", - "@rollup/rollup-linux-powerpc64le-gnu": "4.34.9", - "@rollup/rollup-linux-riscv64-gnu": "4.34.9", - "@rollup/rollup-linux-s390x-gnu": "4.34.9", - "@rollup/rollup-linux-x64-gnu": "4.34.9", - "@rollup/rollup-linux-x64-musl": "4.34.9", - "@rollup/rollup-win32-arm64-msvc": "4.34.9", - "@rollup/rollup-win32-ia32-msvc": "4.34.9", - "@rollup/rollup-win32-x64-msvc": "4.34.9", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/rxjs": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/shell-quote": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", - "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stable-hash": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", - "integrity": "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/synckit": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", - "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@pkgr/core": "^0.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts" - } - }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz", - "integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.4.3", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", - "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "license": "MIT", - "bin": { - "tree-kill": "cli.js" - } - }, - "node_modules/ts-api-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", - "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite": { - "version": "5.4.14", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz", - "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", - "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/website/other/bezier-rs-demos/package.json b/website/other/bezier-rs-demos/package.json deleted file mode 100644 index af2420c66c..0000000000 --- a/website/other/bezier-rs-demos/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "bezier-rs-demos", - "version": "0.1.0", - "private": true, - "type": "module", - "scripts": { - "---------- DEV SERVER ----------": "", - "start": "npm run setup && npm run wasm:build-dev && concurrently -k -n \"VITE,RUST\" \"vite\" \"npm run wasm:watch-dev\"", - "profiling": "npm run setup && npm run wasm:build-profiling && concurrently -k -n \"VITE,RUST\" \"vite\" \"npm run wasm:watch-profiling\"", - "production": "npm run setup && npm run wasm:build-production && concurrently -k -n \"VITE,RUST\" \"vite\" \"npm run wasm:watch-production\"", - "---------- BUILDS ----------": "", - "build-dev": "npm run wasm:build-dev && vite build", - "build-profiling": "npm run wasm:build-profiling && vite build", - "build": "npm run wasm:build-production && vite build", - "---------- UTILITIES ----------": "", - "lint": "ESLINT_USE_FLAT_CONFIG=false eslint . && tsc --noEmit", - "lint-fix": "ESLINT_USE_FLAT_CONFIG=false eslint . --fix && tsc --noEmit", - "---------- INTERNAL ----------": "", - "setup": "node package-installer.js", - "wasm:build-dev": "wasm-pack build ./wasm --dev --target=web", - "wasm:build-profiling": "wasm-pack build ./wasm --profiling --target=web", - "wasm:build-production": "wasm-pack build ./wasm --release --target=web", - "wasm:watch-dev": "cargo watch --postpone --watch-when-idle --workdir=wasm --shell \"wasm-pack build . --dev --target=web -- --color=always\"", - "wasm:watch-profiling": "cargo watch --postpone --watch-when-idle --workdir=wasm --shell \"wasm-pack build . --profiling --target=web -- --color=always\"", - "wasm:watch-production": "cargo watch --postpone --watch-when-idle --workdir=wasm --shell \"wasm-pack build . --release --target=web -- --color=always\"" - }, - "devDependencies": { - "@types/node": "^22.6.1", - "@typescript-eslint/eslint-plugin": "^8.7.0", - "@typescript-eslint/parser": "^8.7.0", - "concurrently": "^9.0.1", - "eslint-config-prettier": "^9.1.0", - "eslint-import-resolver-typescript": "^3.6.3", - "eslint-plugin-import": "^2.30.0", - "eslint-plugin-prettier": "^5.2.1", - "prettier": "^3.3.3", - "ts-node": "^10.9.2", - "typescript": "^5.6.2", - "vite": "^5.4.7" - } -} diff --git a/website/other/bezier-rs-demos/public/style.css b/website/other/bezier-rs-demos/public/style.css deleted file mode 100644 index 48ee23de26..0000000000 --- a/website/other/bezier-rs-demos/public/style.css +++ /dev/null @@ -1,206 +0,0 @@ -:root { - --color-navy: #16323f; - --color-gray: #cccccc; - --range-fill-dark: var(--color-navy); - --range-fill-light: var(--color-gray); - --range-thumb-height: 16px; -} - -html, -body { - font-family: "Inter Variable", sans-serif; - text-align: center; - background-color: white; -} - -.website-header { - color: var(--color-navy); - font-family: "Bona Nova", serif; -} - -.website-description { - font-weight: 500; - color: var(--color-navy); -} - -.category-header { - color: var(--color-navy); - font-family: "Bona Nova", serif; - margin-bottom: 0 -} - -body > h1 { - margin: 40px 0; -} - -body > h1 ~ :last-child { - margin-bottom: 40px; -} - -body > h1 + p { - max-width: 768px; - line-height: 1.4; - margin: auto; - text-align: justify; -} - -body > h2 { - margin-top: 40px; -} - -/* Demo group styles */ -.demo-row { - display: flex; - flex-direction: row; - justify-content: center; -} - -.t-variant-choice { - margin-top: 20px; -} - -.demo-group-header { - display: inline-block; - position: relative; - margin-top: 2em; - margin-bottom: 0; - padding: 0 1em; - font-family: "Bona Nova", serif; - color: var(--color-navy); -} - -.demo-group-header a { - display: none; - position: absolute; - left: 0; - text-decoration: none; - color: inherit; - opacity: 0.5; -} - -.demo-group-header:hover a { - display: inline-block; -} - -.demo-group-container { - position: relative; - width: fit-content; - margin: auto; -} - -/* Demo styles */ -.demo-header { - font-family: "Bona Nova", serif; - color: var(--color-navy); - margin-top: 10px; - margin-bottom: 5px; -} - -.demo-figure { - width: 250px; - height: 200px; - margin: 10px; - border: solid 1px black; -} - -.parent-input-container { - display: flex; - justify-content: center; - flex-direction: column; - align-items: center; -} - -svg text { - pointer-events: none; - -webkit-user-select: none; - user-select: none; -} - -/* Slider Styles */ -.slider-container { - /* width: fit-content; */ - width: 250px; - padding-bottom: 5px; -} - -.input-label { - font-family: monospace; - display: flex; - justify-content: left; -} - -input[type="range"] { - -webkit-appearance: none; - appearance: none; - margin-right: 15px; - width: 250px; - height: 7px; - background: rgba(255, 255, 255, 0.6); - border-radius: 5px; - background: linear-gradient(var(--range-fill-dark), var(--range-fill-dark)) 0 / calc(0.5 * var(--range-thumb-height) + var(--range-ratio) * (100% - var(--range-thumb-height))) var(--range-fill-light); - background-repeat: no-repeat; -} - -/* Input Thumb */ -input[type="range"]::-webkit-slider-thumb { - -webkit-appearance: none; - height: var(--range-thumb-height); - width: var(--range-thumb-height); - border-radius: 50%; - background: var(--range-fill-dark); - box-shadow: 0 0 2px 0 #555; - transition: background .3s ease-in-out; -} - -input[type="range"]::-moz-range-thumb { - -webkit-appearance: none; - appearance: none; - height: var(--range-thumb-height); - width: var(--range-thumb-height); - border-radius: 50%; - background: var(--range-fill-dark); - box-shadow: 0 0 2px 0 #555; - transition: background .3s ease-in-out; -} - -input[type="range"]::-webkit-slider-thumb:hover { - background: linear-gradient(0deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)), var(--range-fill-dark); -} - -input[type="range"]::-moz-range-thumb:hover { - background: linear-gradient(0deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)), var(--range-fill-dark); -} - -/* Input Track */ -input[type=range]::-webkit-slider-runnable-track { - -webkit-appearance: none; - box-shadow: none; - border: none; - background: transparent; - background: none; -} - -input[type=range]::-moz-range-track { - -webkit-appearance: none; - appearance: none; - box-shadow: none; - border: none; - background: transparent; - background: none; - ; -} - -/* Select Styles */ -select { - font-family: monospace; -} - -.select-container { - width: 250px; - padding-bottom: 5px; - display: flex; -} - -.select-input { - margin-left: 8px; -} diff --git a/website/other/bezier-rs-demos/src/features-bezier.ts b/website/other/bezier-rs-demos/src/features-bezier.ts deleted file mode 100644 index f9c6f5d83e..0000000000 --- a/website/other/bezier-rs-demos/src/features-bezier.ts +++ /dev/null @@ -1,589 +0,0 @@ -import { WasmBezier } from "@/../wasm/pkg"; -import type { BezierDemoOptions, WasmBezierInstance, BezierCallback, InputOption } from "@/types"; -import { capOptions, tSliderOptions, bezierTValueVariantOptions, errorOptions, minimumSeparationOptions, BEZIER_T_VALUE_VARIANTS } from "@/types"; - -const bezierFeatures = { - constructor: { - name: "Constructor", - callback: (bezier: WasmBezierInstance, _: Record): string => bezier.to_svg(), - }, - "bezier-through-points": { - name: "Bezier Through Points", - callback: (bezier: WasmBezierInstance, options: Record): string => { - const points = bezier.get_points(); - if (Object.values(options).length === 1) { - return WasmBezier.quadratic_through_points(points, options.t); - } - return WasmBezier.cubic_through_points(points, options.t, options["midpoint separation"]); - }, - demoOptions: { - Linear: { - disabled: true, - }, - Quadratic: { - customPoints: [ - [30, 50], - [120, 70], - [160, 170], - ], - inputOptions: [ - { - variable: "t", - inputType: "slider", - min: 0.01, - max: 0.99, - step: 0.01, - default: 0.5, - }, - ], - }, - Cubic: { - customPoints: [ - [30, 50], - [120, 70], - [160, 170], - ], - inputOptions: [ - { - variable: "t", - inputType: "slider", - min: 0.01, - max: 0.99, - step: 0.01, - default: 0.5, - }, - { - variable: "midpoint separation", - inputType: "slider", - min: 0, - max: 100, - step: 2, - default: 30, - }, - ], - }, - }, - }, - length: { - name: "Length", - callback: (bezier: WasmBezierInstance, _: Record): string => bezier.length(), - }, - "length-centroid": { - name: "Length Centroid", - callback: (bezier: WasmBezierInstance, _: Record): string => bezier.length_centroid(), - }, - evaluate: { - name: "Evaluate", - callback: (bezier: WasmBezierInstance, options: Record, _: undefined): string => bezier.evaluate(options.t, BEZIER_T_VALUE_VARIANTS[options.TVariant]), - demoOptions: { - Quadratic: { - inputOptions: [bezierTValueVariantOptions, tSliderOptions], - }, - }, - }, - "lookup-table": { - name: "Lookup Table", - callback: (bezier: WasmBezierInstance, options: Record, _: undefined): string => bezier.compute_lookup_table(options.steps, BEZIER_T_VALUE_VARIANTS[options.TVariant]), - demoOptions: { - Quadratic: { - inputOptions: [ - bezierTValueVariantOptions, - { - variable: "steps", - inputType: "slider", - min: 2, - max: 15, - step: 1, - default: 5, - }, - ], - }, - }, - }, - derivative: { - name: "Derivative", - callback: (bezier: WasmBezierInstance, _: Record): string => bezier.derivative(), - demoOptions: { - Linear: { - disabled: true, - }, - Quadratic: { - customPoints: [ - [30, 40], - [110, 50], - [120, 130], - ], - }, - Cubic: { - customPoints: [ - [50, 50], - [60, 100], - [100, 140], - [140, 150], - ], - }, - }, - }, - tangent: { - name: "Tangent", - callback: (bezier: WasmBezierInstance, options: Record, _: undefined): string => bezier.tangent(options.t, BEZIER_T_VALUE_VARIANTS[options.TVariant]), - demoOptions: { - Quadratic: { - inputOptions: [bezierTValueVariantOptions, tSliderOptions], - }, - }, - }, - "tangents-to-point": { - name: "Tangents To Point", - callback: (bezier: WasmBezierInstance, _: Record, mouseLocation?: [number, number]): string => - mouseLocation ? bezier.tangents_to_point(mouseLocation[0], mouseLocation[1]) : bezier.to_svg(), - triggerOnMouseMove: true, - demoOptions: { Linear: { disabled: true } }, - }, - normal: { - name: "Normal", - callback: (bezier: WasmBezierInstance, options: Record, _: undefined): string => bezier.normal(options.t, BEZIER_T_VALUE_VARIANTS[options.TVariant]), - demoOptions: { - Quadratic: { - inputOptions: [bezierTValueVariantOptions, tSliderOptions], - }, - }, - }, - "normals-to-point": { - name: "Normals To Point", - callback: (bezier: WasmBezierInstance, _: Record, mouseLocation?: [number, number]): string => - mouseLocation ? bezier.normals_to_point(mouseLocation[0], mouseLocation[1]) : bezier.to_svg(), - triggerOnMouseMove: true, - }, - curvature: { - name: "Curvature", - callback: (bezier: WasmBezierInstance, options: Record, _: undefined): string => bezier.curvature(options.t, BEZIER_T_VALUE_VARIANTS[options.TVariant]), - demoOptions: { - Linear: { - disabled: true, - }, - Quadratic: { - inputOptions: [bezierTValueVariantOptions, tSliderOptions], - }, - Cubic: { - inputOptions: [bezierTValueVariantOptions, { ...tSliderOptions, default: 0.7 }], - }, - }, - }, - split: { - name: "Split", - callback: (bezier: WasmBezierInstance, options: Record, _: undefined): string => bezier.split(options.t, BEZIER_T_VALUE_VARIANTS[options.TVariant]), - demoOptions: { - Quadratic: { - inputOptions: [bezierTValueVariantOptions, tSliderOptions], - }, - }, - }, - trim: { - name: "Trim", - callback: (bezier: WasmBezierInstance, options: Record, _: undefined): string => bezier.trim(options.t1, options.t2, BEZIER_T_VALUE_VARIANTS[options.TVariant]), - demoOptions: { - Quadratic: { - inputOptions: [ - bezierTValueVariantOptions, - { - variable: "t1", - inputType: "slider", - min: 0, - max: 1, - step: 0.01, - default: 0.25, - }, - { - variable: "t2", - inputType: "slider", - min: 0, - max: 1, - step: 0.01, - default: 0.75, - }, - ], - }, - }, - }, - project: { - name: "Project", - callback: (bezier: WasmBezierInstance, _: Record, mouseLocation?: [number, number]): string => - mouseLocation ? bezier.project(mouseLocation[0], mouseLocation[1]) : bezier.to_svg(), - triggerOnMouseMove: true, - }, - "local-extrema": { - name: "Local Extrema", - callback: (bezier: WasmBezierInstance, _: Record): string => bezier.local_extrema(), - demoOptions: { - Linear: { - disabled: true, - }, - Quadratic: { - customPoints: [ - [40, 40], - [160, 30], - [110, 150], - ], - }, - Cubic: { - customPoints: [ - [160, 180], - [170, 10], - [30, 90], - [180, 160], - ], - }, - }, - }, - "bounding-box": { - name: "Bounding Box", - callback: (bezier: WasmBezierInstance, _: Record): string => bezier.bounding_box(), - }, - inflections: { - name: "Inflections", - callback: (bezier: WasmBezierInstance, _: Record): string => bezier.inflections(), - demoOptions: { - Linear: { - disabled: true, - }, - Quadratic: { - disabled: true, - }, - }, - }, - reduce: { - name: "Reduce", - callback: (bezier: WasmBezierInstance): string => bezier.reduce(), - }, - offset: { - name: "Offset", - callback: (bezier: WasmBezierInstance, options: Record): string => bezier.offset(options.distance), - demoOptions: { - Quadratic: { - inputOptions: [ - { - variable: "distance", - inputType: "slider", - min: -30, - max: 30, - step: 1, - default: 15, - }, - ], - }, - }, - }, - outline: { - name: "Outline", - callback: (bezier: WasmBezierInstance, options: Record): string => bezier.outline(options.distance, options.cap), - demoOptions: { - Quadratic: { - inputOptions: [ - { - variable: "distance", - inputType: "slider", - min: 0, - max: 30, - step: 1, - default: 15, - }, - capOptions, - ], - }, - }, - }, - "graduated-outline": { - name: "Graduated Outline", - callback: (bezier: WasmBezierInstance, options: Record): string => bezier.graduated_outline(options.start_distance, options.end_distance, options.cap), - demoOptions: { - Quadratic: { - inputOptions: [ - { - variable: "start_distance", - inputType: "slider", - min: 0, - max: 30, - step: 1, - default: 5, - }, - { - variable: "end_distance", - inputType: "slider", - min: 0, - max: 30, - step: 1, - default: 15, - }, - capOptions, - ], - }, - }, - customPoints: { - Cubic: [ - [31, 94], - [40, 40], - [107, 107], - [106, 106], - ], - }, - }, - "skewed-outline": { - name: "Skewed Outline", - callback: (bezier: WasmBezierInstance, options: Record): string => - bezier.skewed_outline(options.distance1, options.distance2, options.distance3, options.distance4, options.cap), - demoOptions: { - Quadratic: { - inputOptions: [ - { - variable: "distance1", - inputType: "slider", - min: 0, - max: 30, - step: 1, - default: 20, - }, - { - variable: "distance2", - inputType: "slider", - min: 0, - max: 30, - step: 1, - default: 10, - }, - { - variable: "distance3", - inputType: "slider", - min: 0, - max: 30, - step: 1, - default: 30, - }, - { - variable: "distance4", - inputType: "slider", - min: 0, - max: 30, - step: 1, - default: 5, - }, - capOptions, - ], - }, - }, - }, - arcs: { - name: "Arcs", - callback: (bezier: WasmBezierInstance, options: Record): string => bezier.arcs(options.error, options.max_iterations, options.strategy), - demoOptions: ((): BezierDemoOptions => { - const inputOptions: InputOption[] = [ - { - variable: "strategy", - inputType: "dropdown", - default: 0, - options: ["Automatic", "FavorLargerArcs", "FavorCorrectness"], - }, - { - variable: "error", - inputType: "slider", - min: 0.05, - max: 1, - step: 0.05, - default: 0.5, - }, - { - variable: "max_iterations", - inputType: "slider", - min: 50, - max: 200, - step: 1, - default: 100, - }, - ]; - - return { - Linear: { - disabled: true, - }, - Quadratic: { - customPoints: [ - [70, 40], - [180, 50], - [160, 150], - ], - inputOptions, - disabled: false, - }, - Cubic: { - customPoints: [ - [160, 180], - [170, 10], - [30, 90], - [180, 160], - ], - inputOptions, - disabled: false, - }, - }; - })(), - }, - "intersect-linear": { - name: "Intersect (Linear Segment)", - callback: (bezier: WasmBezierInstance): string => { - const line = [ - [45, 30], - [195, 160], - ]; - return bezier.intersect_line_segment(line); - }, - }, - "intersect-quadratic": { - name: "Intersect (Quadratic Segment)", - callback: (bezier: WasmBezierInstance, options: Record): string => { - const quadratic = [ - [45, 80], - [205, 10], - [115, 120], - ]; - return bezier.intersect_quadratic_segment(quadratic, options.error, options.minimum_separation); - }, - demoOptions: { - Quadratic: { - inputOptions: [errorOptions, minimumSeparationOptions], - }, - }, - }, - "intersect-cubic": { - name: "Intersect (Cubic Segment)", - callback: (bezier: WasmBezierInstance, options: Record): string => { - const cubic = [ - [65, 20], - [125, 40], - [65, 120], - [200, 140], - ]; - return bezier.intersect_cubic_segment(cubic, options.error, options.minimum_separation); - }, - demoOptions: { - Quadratic: { - inputOptions: [errorOptions, minimumSeparationOptions], - }, - }, - }, - "intersect-self": { - name: "Intersect (Self)", - callback: (bezier: WasmBezierInstance, options: Record): string => bezier.intersect_self(options.error, options.minimum_separation), - demoOptions: { - Linear: { - disabled: true, - }, - Quadratic: { - disabled: true, - }, - Cubic: { - inputOptions: [errorOptions, minimumSeparationOptions], - customPoints: [ - [160, 180], - [170, 10], - [30, 90], - [180, 140], - ], - }, - }, - }, - "intersect-rectangle": { - name: "Intersect (Rectangle)", - callback: (bezier: WasmBezierInstance): string => - bezier.intersect_rectangle([ - [75, 50], - [175, 150], - ]), - }, - rotate: { - name: "Rotate", - callback: (bezier: WasmBezierInstance, options: Record): string => bezier.rotate(options.angle * Math.PI, 125, 100), - demoOptions: { - Quadratic: { - inputOptions: [ - { - variable: "angle", - inputType: "slider", - min: 0, - max: 2, - step: 1 / 50, - default: 0.12, - unit: "π", - }, - ], - }, - }, - }, - "de-casteljau-points": { - name: "De Casteljau Points", - callback: (bezier: WasmBezierInstance, options: Record, _: undefined): string => bezier.de_casteljau_points(options.t, BEZIER_T_VALUE_VARIANTS[options.TVariant]), - demoOptions: { - Quadratic: { - inputOptions: [bezierTValueVariantOptions, tSliderOptions], - }, - }, - }, - join: { - name: "Join", - callback: (bezier: WasmBezierInstance): string => { - const points = bezier.get_points(); - let examplePoints = []; - if (points.length === 2) { - examplePoints = [ - [145, 155], - [65, 155], - ]; - } else if (points.length === 3) { - examplePoints = [ - [65, 150], - [120, 195], - [190, 145], - ]; - } else { - examplePoints = [ - [165, 150], - [110, 110], - [90, 180], - [55, 140], - ]; - } - return bezier.join(examplePoints); - }, - demoOptions: { - Linear: { - customPoints: [ - [70, 40], - [155, 90], - ], - }, - Quadratic: { - customPoints: [ - [185, 40], - [65, 20], - [100, 85], - ], - }, - Cubic: { - customPoints: [ - [45, 80], - [65, 20], - [115, 100], - [155, 55], - ], - }, - }, - }, -}; - -export type BezierFeatureKey = keyof typeof bezierFeatures; -export type BezierFeatureOptions = { - name: string; - callback: BezierCallback; - demoOptions?: Partial; - triggerOnMouseMove?: boolean; -}; -export default bezierFeatures as Record; diff --git a/website/other/bezier-rs-demos/src/features-subpath.ts b/website/other/bezier-rs-demos/src/features-subpath.ts deleted file mode 100644 index 9568275ed0..0000000000 --- a/website/other/bezier-rs-demos/src/features-subpath.ts +++ /dev/null @@ -1,252 +0,0 @@ -import type { SubpathCallback, SubpathInputOption, WasmSubpathInstance } from "@/types"; -import { capOptions, joinOptions, tSliderOptions, subpathTValueVariantOptions, intersectionErrorOptions, minimumSeparationOptions, separationDiskDiameter, SUBPATH_T_VALUE_VARIANTS } from "@/types"; - -const subpathFeatures = { - constructor: { - name: "Constructor", - callback: (subpath: WasmSubpathInstance): string => subpath.to_svg(), - }, - insert: { - name: "Insert", - callback: (subpath: WasmSubpathInstance, options: Record, _: undefined): string => subpath.insert(options.t, SUBPATH_T_VALUE_VARIANTS[options.TVariant]), - inputOptions: [subpathTValueVariantOptions, tSliderOptions], - }, - length: { - name: "Length", - callback: (subpath: WasmSubpathInstance): string => subpath.length(), - }, - "length-centroid": { - name: "Length Centroid", - callback: (subpath: WasmSubpathInstance): string => subpath.length_centroid(), - }, - area: { - name: "Area", - callback: (subpath: WasmSubpathInstance, options: Record, _: undefined): string => subpath.area(options.error, options.minimum_separation), - inputOptions: [intersectionErrorOptions, minimumSeparationOptions], - }, - "area-centroid": { - name: "Area Centroid", - callback: (subpath: WasmSubpathInstance, options: Record, _: undefined): string => subpath.area_centroid(options.error, options.minimum_separation), - inputOptions: [intersectionErrorOptions, minimumSeparationOptions], - }, - "poisson-disk-points": { - name: "Poisson-Disk Points", - callback: (subpath: WasmSubpathInstance, options: Record, _: undefined): string => subpath.poisson_disk_points(options.separation_disk_diameter), - inputOptions: [separationDiskDiameter], - }, - evaluate: { - name: "Evaluate", - callback: (subpath: WasmSubpathInstance, options: Record, _: undefined): string => subpath.evaluate(options.t, SUBPATH_T_VALUE_VARIANTS[options.TVariant]), - inputOptions: [subpathTValueVariantOptions, tSliderOptions], - }, - "lookup-table": { - name: "Lookup Table", - callback: (subpath: WasmSubpathInstance, options: Record, _: undefined): string => subpath.compute_lookup_table(options.steps, SUBPATH_T_VALUE_VARIANTS[options.TVariant]), - inputOptions: [ - subpathTValueVariantOptions, - { - variable: "steps", - inputType: "slider", - min: 2, - max: 30, - step: 1, - default: 5, - }, - ], - }, - project: { - name: "Project", - callback: (subpath: WasmSubpathInstance, _: Record, mouseLocation?: [number, number]): string => - mouseLocation ? subpath.project(mouseLocation[0], mouseLocation[1]) : subpath.to_svg(), - triggerOnMouseMove: true, - }, - tangent: { - name: "Tangent", - callback: (subpath: WasmSubpathInstance, options: Record, _: undefined): string => subpath.tangent(options.t, SUBPATH_T_VALUE_VARIANTS[options.TVariant]), - inputOptions: [subpathTValueVariantOptions, tSliderOptions], - }, - normal: { - name: "Normal", - callback: (subpath: WasmSubpathInstance, options: Record, _: undefined): string => subpath.normal(options.t, SUBPATH_T_VALUE_VARIANTS[options.TVariant]), - inputOptions: [subpathTValueVariantOptions, tSliderOptions], - }, - "local-extrema": { - name: "Local Extrema", - callback: (subpath: WasmSubpathInstance): string => subpath.local_extrema(), - }, - "bounding-box": { - name: "Bounding Box", - callback: (subpath: WasmSubpathInstance): string => subpath.bounding_box(), - }, - inflections: { - name: "Inflections", - callback: (subpath: WasmSubpathInstance): string => subpath.inflections(), - }, - "intersect-linear": { - name: "Intersect (Linear Segment)", - callback: (subpath: WasmSubpathInstance, options: Record): string => - subpath.intersect_line_segment( - [ - [80, 30], - [210, 150], - ], - options.error, - options.minimum_separation, - ), - inputOptions: [intersectionErrorOptions, minimumSeparationOptions], - }, - "intersect-quadratic": { - name: "Intersect (Quadratic Segment)", - callback: (subpath: WasmSubpathInstance, options: Record): string => - subpath.intersect_quadratic_segment( - [ - [25, 50], - [205, 10], - [135, 180], - ], - options.error, - options.minimum_separation, - ), - inputOptions: [intersectionErrorOptions, minimumSeparationOptions], - }, - "intersect-cubic": { - name: "Intersect (Cubic Segment)", - callback: (subpath: WasmSubpathInstance, options: Record): string => - subpath.intersect_cubic_segment( - [ - [65, 20], - [125, 40], - [65, 120], - [200, 140], - ], - options.error, - options.minimum_separation, - ), - inputOptions: [intersectionErrorOptions, minimumSeparationOptions], - }, - "intersect-self": { - name: "Intersect (Self)", - callback: (subpath: WasmSubpathInstance, options: Record): string => subpath.self_intersections(options.error, options.minimum_separation), - inputOptions: [intersectionErrorOptions, minimumSeparationOptions], - }, - "intersect-rectangle": { - name: "Intersect (Rectangle)", - callback: (subpath: WasmSubpathInstance, options: Record): string => - subpath.intersect_rectangle( - [ - [75, 50], - [175, 150], - ], - options.error, - options.minimum_separation, - ), - inputOptions: [intersectionErrorOptions, minimumSeparationOptions], - }, - "inside-other": { - name: "Inside (Other Subpath)", - callback: (subpath: WasmSubpathInstance, options: Record): string => - subpath.inside_subpath( - [ - [40, 40], - [160, 40], - [160, 80], - [200, 100], - [160, 120], - [160, 160], - [40, 160], - [40, 120], - [80, 100], - [40, 80], - ], - options.error, - options.minimum_separation, - ), - inputOptions: [intersectionErrorOptions, minimumSeparationOptions], - }, - curvature: { - name: "Curvature", - callback: (subpath: WasmSubpathInstance, options: Record, _: undefined): string => subpath.curvature(options.t, SUBPATH_T_VALUE_VARIANTS[options.TVariant]), - inputOptions: [subpathTValueVariantOptions, { ...tSliderOptions, default: 0.2 }], - }, - split: { - name: "Split", - callback: (subpath: WasmSubpathInstance, options: Record, _: undefined): string => subpath.split(options.t, SUBPATH_T_VALUE_VARIANTS[options.TVariant]), - inputOptions: [subpathTValueVariantOptions, tSliderOptions], - }, - trim: { - name: "Trim", - callback: (subpath: WasmSubpathInstance, options: Record, _: undefined): string => subpath.trim(options.t1, options.t2, SUBPATH_T_VALUE_VARIANTS[options.TVariant]), - inputOptions: [subpathTValueVariantOptions, { ...tSliderOptions, default: 0.2, variable: "t1" }, { ...tSliderOptions, variable: "t2" }], - }, - offset: { - name: "Offset", - callback: (subpath: WasmSubpathInstance, options: Record): string => subpath.offset(options.distance, options.join, options.miter_limit), - inputOptions: [ - { - variable: "distance", - inputType: "slider", - min: -25, - max: 25, - step: 1, - default: 10, - }, - joinOptions, - { - variable: "join: Miter - limit", - inputType: "slider", - min: 1, - max: 10, - step: 0.25, - default: 4, - }, - ], - }, - outline: { - name: "Outline", - callback: (subpath: WasmSubpathInstance, options: Record): string => subpath.outline(options.distance, options.join, options.cap, options.miter_limit), - inputOptions: [ - { - variable: "distance", - inputType: "slider", - min: 0, - max: 25, - step: 1, - default: 10, - }, - joinOptions, - { - variable: "join: Miter - limit", - inputType: "slider", - min: 1, - max: 10, - step: 0.25, - default: 4, - }, - { ...capOptions, isDisabledForClosed: true }, - ], - }, - rotate: { - name: "Rotate", - callback: (subpath: WasmSubpathInstance, options: Record): string => subpath.rotate(options.angle * Math.PI, 125, 100), - inputOptions: [ - { - variable: "angle", - inputType: "slider", - min: 0, - max: 2, - step: 1 / 50, - default: 0.12, - unit: "π", - }, - ], - }, -}; - -export type SubpathFeatureKey = keyof typeof subpathFeatures; -export type SubpathFeatureOptions = { - name: string; - callback: SubpathCallback; - inputOptions?: SubpathInputOption[]; - triggerOnMouseMove?: boolean; -}; -export default subpathFeatures as Record; diff --git a/website/other/bezier-rs-demos/src/main.ts b/website/other/bezier-rs-demos/src/main.ts deleted file mode 100644 index d01826a0f4..0000000000 --- a/website/other/bezier-rs-demos/src/main.ts +++ /dev/null @@ -1,345 +0,0 @@ -import { default as init, WasmSubpath, WasmBezier } from "@/../wasm/pkg"; -import bezierFeatures from "@/features-bezier"; -import type { BezierFeatureKey, BezierFeatureOptions } from "@/features-bezier"; -import subpathFeatures from "@/features-subpath"; -import type { SubpathFeatureKey, SubpathFeatureOptions } from "@/features-subpath"; -import type { DemoArgs, BezierCurveType, BezierDemoArgs, SubpathDemoArgs, DemoData, WasmSubpathInstance, WasmSubpathManipulatorKey, InputOption, DemoDataBezier, DemoDataSubpath } from "@/types"; -import { BEZIER_CURVE_TYPE, getBezierDemoPointDefaults, getSubpathDemoArgs, POINT_INDEX_TO_MANIPULATOR, getConstructorKey, getCurveType, MANIPULATOR_KEYS_FROM_BEZIER_TYPE } from "@/types"; - -init().then(renderPage); - -function renderPage() { - // Determine whether the page needs to recompute which examples to show - window.addEventListener("hashchange", (e: HashChangeEvent) => { - const isUrlSolo = (url: string) => { - const splitHash = url.split("#")?.[1]?.split("/"); - return splitHash?.length === 3 && splitHash?.[2] === "solo"; - }; - - const isOldHashSolo = isUrlSolo(e.oldURL); - const isNewHashSolo = isUrlSolo(e.newURL); - const target = document.getElementById(window.location.hash.substring(1)); - if (!target || isOldHashSolo !== isNewHashSolo) renderPage(); - }); - - // Get the hash from the URL - const hash = window.location.hash; - const splitHash = hash.split("/"); - - // Scroll to specified hash if it exists - if (hash) document.getElementById(hash.substring(1))?.scrollIntoView(); - - // Determine which examples to render based on hash - if (splitHash[0] === "#bezier" && splitHash[1] in bezierFeatures && splitHash[2] === "solo") { - window.document.body.innerHTML = `
`; - const container = document.getElementById("bezier-demos"); - if (!container) return; - - const key = splitHash[1]; - const value = (bezierFeatures as Record)[key]; - if (value) container.append(bezierDemoGroup(key as BezierFeatureKey, value)); - - return; - } - - if (splitHash[0] === "#subpath" && splitHash[1] in subpathFeatures && splitHash[2] === "solo") { - window.document.body.innerHTML = `
`; - const container = document.getElementById("subpath-demos"); - if (!container) return; - - const key = splitHash[1]; - const value = (subpathFeatures as Record)[key]; - if (value) container.append(subpathDemoGroup(key as SubpathFeatureKey, value)); - - return; - } - - window.document.body.innerHTML = ` -

Bezier-rs Interactive Documentation

-

- This is the interactive documentation for the Bezier-rs library. View the - crate documentation - for detailed function descriptions and API usage. Click and drag on the endpoints of the demo curves to visualize the various Bezier utilities and functions. -

- -

Beziers

-
- -

Subpaths

-
- `.trim(); - - const bezierDemos = document.getElementById("bezier-demos") || undefined; - if (bezierDemos) Object.entries(bezierFeatures).forEach(([key, options]) => bezierDemos.appendChild(bezierDemoGroup(key as BezierFeatureKey, options))); - - const subpathDemos = document.getElementById("subpath-demos") || undefined; - if (subpathDemos) Object.entries(subpathFeatures).forEach(([key, options]) => subpathDemos.appendChild(subpathDemoGroup(key as SubpathFeatureKey, options))); -} - -function bezierDemoGroup(key: BezierFeatureKey, options: BezierFeatureOptions): HTMLDivElement { - const demoOptions = options.demoOptions || {}; - const demos: BezierDemoArgs[] = BEZIER_CURVE_TYPE.map((curveType: BezierCurveType) => ({ - title: curveType, - disabled: demoOptions[curveType]?.disabled || false, - points: demoOptions[curveType]?.customPoints || getBezierDemoPointDefaults()[curveType], - inputOptions: demoOptions[curveType]?.inputOptions || demoOptions.Quadratic?.inputOptions || [], - })); - return renderDemoGroup(`bezier/${key}`, bezierFeatures[key].name, demos, (demo: BezierDemoArgs) => - demoBezier(demo.title, demo.points, key, demo.inputOptions, options.triggerOnMouseMove || false), - ); -} - -function subpathDemoGroup(key: SubpathFeatureKey, options: SubpathFeatureOptions): HTMLDivElement { - const buildDemo = (demo: SubpathDemoArgs) => { - const newInputOptions = (options.inputOptions || []).map((option) => ({ - ...option, - disabled: option.isDisabledForClosed && demo.closed, - })); - return demoSubpath(demo.title, demo.triples, key, demo.closed, newInputOptions, options.triggerOnMouseMove || false); - }; - return renderDemoGroup(`subpath/${key}`, subpathFeatures[key].name, getSubpathDemoArgs(), buildDemo); -} - -function demoBezier(title: string, points: number[][], key: BezierFeatureKey, inputOptions: InputOption[], triggerOnMouseMove: boolean): DemoDataBezier { - return { - kind: "bezier", - title, - element: document.createElement("div"), - inputOptions, - locked: false, - triggerOnMouseMove, - sliderData: Object.assign({}, ...inputOptions.map((s) => ({ [s.variable]: s.default }))), - sliderUnits: Object.assign({}, ...inputOptions.map((s) => ({ [s.variable]: s.unit }))), - activePointIndex: undefined as number | undefined, - manipulatorKeys: MANIPULATOR_KEYS_FROM_BEZIER_TYPE[getCurveType(points.length)], - bezier: WasmBezier[getConstructorKey(getCurveType(points.length))](points), - points, - callback: bezierFeatures[key].callback, - }; -} - -function demoSubpath(title: string, triples: (number[] | undefined)[][], key: SubpathFeatureKey, closed: boolean, inputOptions: InputOption[], triggerOnMouseMove: boolean): DemoDataSubpath { - return { - kind: "subpath", - title, - element: document.createElement("div"), - inputOptions, - locked: false, - triggerOnMouseMove, - sliderData: Object.assign({}, ...inputOptions.map((s) => ({ [s.variable]: s.default }))), - sliderUnits: Object.assign({}, ...inputOptions.map((s) => ({ [s.variable]: s.unit }))), - activePointIndex: undefined as number | undefined, - activeManipulatorIndex: undefined as number | undefined, - manipulatorKeys: undefined as undefined | WasmSubpathManipulatorKey[], - subpath: WasmSubpath.from_triples(triples, closed) as WasmSubpathInstance, - triples, - callback: subpathFeatures[key].callback, - }; -} - -function updateDemoSVG(data: DemoData, figure: HTMLElement, mouseLocation?: [number, number]) { - if (data.kind === "subpath") figure.innerHTML = data.callback(data.subpath, data.sliderData, mouseLocation); - if (data.kind === "bezier") figure.innerHTML = data.callback(data.bezier, data.sliderData, mouseLocation); -} - -function onMouseDown(data: DemoData, e: MouseEvent) { - const SELECTABLE_RANGE = 10; - - let distances; - if (data.kind === "bezier") { - distances = data.points.flatMap((point, pointIndex) => { - if (!point) return []; - const distance = Math.sqrt(Math.pow(e.offsetX - point[0], 2) + Math.pow(e.offsetY - point[1], 2)); - return distance < SELECTABLE_RANGE ? [{ manipulatorIndex: undefined, pointIndex, distance }] : []; - }); - } else if (data.kind === "subpath") { - distances = data.triples.flatMap((triple, manipulatorIndex) => - triple.flatMap((point, pointIndex) => { - if (!point) return []; - const distance = Math.sqrt(Math.pow(e.offsetX - point[0], 2) + Math.pow(e.offsetY - point[1], 2)); - return distance < SELECTABLE_RANGE ? [{ manipulatorIndex, pointIndex, distance }] : []; - }), - ); - } else { - return; - } - - const closest = distances.sort((a, b) => a.distance - b.distance)[0]; - if (closest) { - if (data.kind === "subpath") data.activeManipulatorIndex = closest.manipulatorIndex; - data.activePointIndex = closest.pointIndex; - } -} - -function onMouseMove(data: DemoData, e: MouseEvent) { - if (data.locked || !(e.currentTarget instanceof HTMLElement)) return; - data.locked = true; - - if (data.kind === "bezier" && data.activePointIndex !== undefined) { - data.bezier[data.manipulatorKeys[data.activePointIndex]](e.offsetX, e.offsetY); - data.points[data.activePointIndex] = [e.offsetX, e.offsetY]; - - updateDemoSVG(data, e.currentTarget); - } else if (data.kind === "subpath" && data.activePointIndex !== undefined && data.activeManipulatorIndex !== undefined) { - data.subpath[POINT_INDEX_TO_MANIPULATOR[data.activePointIndex]](data.activeManipulatorIndex, e.offsetX, e.offsetY); - data.triples[data.activeManipulatorIndex][data.activePointIndex] = [e.offsetX, e.offsetY]; - - updateDemoSVG(data, e.currentTarget); - } else if (data.triggerOnMouseMove) { - updateDemoSVG(data, e.currentTarget, [e.offsetX, e.offsetY]); - } - - data.locked = false; -} - -function onMouseUp(data: DemoData) { - data.activePointIndex = undefined; - if (data.kind === "subpath") data.activeManipulatorIndex = undefined; -} - -function renderDemoGroup(id: string, name: string, demos: T[], buildDemo: (demo: T) => DemoData): HTMLDivElement { - const demoGroup = document.createElement("div"); - demoGroup.className = "demo-group-container"; - - demoGroup.insertAdjacentHTML( - "beforeend", - ` - ${(() => { - // Add header and href anchor if not on a solo example page - const currentHash = window.location.hash.split("/"); - if (currentHash.length === 3 || currentHash[2] === "solo") return ""; - return ` -

- # - ${name} -

- `.trim(); - })()} -
- `.trim(), - ); - - const demoRow = demoGroup.querySelector("[data-demo-row]"); - if (!demoRow) return demoGroup; - - demos.forEach((demo) => { - if (demo.disabled) return; - const data = buildDemo(demo); - - renderDemo(data); - - const figure = data.element.querySelector("[data-demo-figure]"); - if (figure instanceof HTMLElement) updateDemoSVG(data, figure); - - demoRow.append(data.element); - }); - - return demoGroup; -} - -function renderDemo(demo: DemoData) { - const getSliderUnit = (data: DemoData, variable: string): string => { - return (Array.isArray(data.sliderUnits[variable]) ? "" : data.sliderUnits[variable]) || ""; - }; - - demo.element.insertAdjacentHTML( - "beforeend", - ` -

${demo.title}

-
-
- ${(() => - demo.inputOptions - .map((inputOption) => - ` -
-
- ${inputOption.variable}: ${inputOption.inputType === "dropdown" ? "" : demo.sliderData[inputOption.variable]}${getSliderUnit(demo, inputOption.variable)} -
- ${(() => { - if (inputOption.inputType !== "dropdown") return ""; - return ` - - `.trim(); - })()} - ${(() => { - if (inputOption.inputType !== "slider") return ""; - const ratio = (Number(inputOption.default) - (inputOption.min || 0)) / ((inputOption.max || 100) - (inputOption.min || 0)); - return ` - - `.trim(); - })()} -
- `.trim(), - ) - .join("\n"))()} -
- `.trim(), - ); - - const figure = demo.element.querySelector(`[data-demo-figure]`); - if (!(figure instanceof HTMLElement)) return; - figure.addEventListener("mousedown", (e) => onMouseDown(demo, e)); - figure.addEventListener("mouseup", () => onMouseUp(demo)); - figure.addEventListener("mousemove", (e) => onMouseMove(demo, e)); - - demo.inputOptions.forEach((inputOption, index) => { - const inputContainer = demo.element.querySelectorAll(`[data-parent-input-container] [data-input-container]`)[index]; - if (!(inputContainer instanceof HTMLDivElement)) return; - - if (inputOption.inputType === "dropdown") { - const selectElement = inputContainer.querySelector("[data-select]"); - if (!(selectElement instanceof HTMLSelectElement)) return; - - selectElement.addEventListener("change", (e: Event) => { - if (!(e.target instanceof HTMLSelectElement)) return; - - demo.sliderData[inputOption.variable] = Number(e.target.value); - updateDemoSVG(demo, figure); - }); - } - - if (inputOption.inputType === "slider") { - const sliderInput = inputContainer.querySelector("[data-slider-input]"); - if (!(sliderInput instanceof HTMLInputElement)) return; - - sliderInput.addEventListener("input", (e: Event) => { - const target = e.target; - if (!(target instanceof HTMLInputElement)) return; - - // Set the slider label text - const variable = inputOption.variable; - const data = demo.sliderData[variable]; - const unit = getSliderUnit(demo, variable); - const label = inputContainer.querySelector("[data-input-label]"); - if (!(label instanceof HTMLDivElement)) return; - label.innerText = `${variable}: ${data}${unit}`; - - // Set the slider input range percentage - sliderInput.style.setProperty("--range-ratio", String((Number(target.value) - (inputOption.min || 0)) / ((inputOption.max || 100) - (inputOption.min || 0)))); - - // Update the slider data and redraw the demo - demo.sliderData[variable] = Number(target.value); - updateDemoSVG(demo, figure); - }); - } - }); -} diff --git a/website/other/bezier-rs-demos/src/types.ts b/website/other/bezier-rs-demos/src/types.ts deleted file mode 100644 index 669212f64e..0000000000 --- a/website/other/bezier-rs-demos/src/types.ts +++ /dev/null @@ -1,241 +0,0 @@ -import type * as WasmPkg from "@/../wasm/pkg"; - -type WasmRawInstance = typeof WasmPkg; -export type WasmBezierInstance = InstanceType; - -export type WasmSubpathInstance = InstanceType; -export type WasmSubpathManipulatorKey = "set_anchor" | "set_in_handle" | "set_out_handle"; -type WasmBezierConstructorKey = "new_linear" | "new_quadratic" | "new_cubic"; -type WasmBezierManipulatorKey = "set_start" | "set_handle_start" | "set_handle_end" | "set_end"; - -type DemoDataCommon = { - title: string; - element: HTMLDivElement; - inputOptions: InputOption[]; - locked: boolean; - triggerOnMouseMove: boolean; - sliderData: Record; - sliderUnits: Record; - activePointIndex: number | undefined; -}; -export type DemoDataBezier = DemoDataCommon & { - kind: "bezier"; - manipulatorKeys: WasmBezierManipulatorKey[]; - bezier: WasmBezierInstance; - points: number[][]; - callback: BezierCallback; -}; -export type DemoDataSubpath = DemoDataCommon & { - kind: "subpath"; - activeManipulatorIndex: number | undefined; - manipulatorKeys: WasmSubpathManipulatorKey[] | undefined; - subpath: WasmSubpathInstance; - triples: (number[] | undefined)[][]; - callback: SubpathCallback; -}; -export type DemoData = DemoDataBezier | DemoDataSubpath; - -export const BEZIER_CURVE_TYPE = ["Linear", "Quadratic", "Cubic"] as const; -export type BezierCurveType = (typeof BEZIER_CURVE_TYPE)[number]; - -export type BezierCallback = (bezier: WasmBezierInstance, options: Record, mouseLocation?: [number, number]) => string; -export type SubpathCallback = (subpath: WasmSubpathInstance, options: Record, mouseLocation?: [number, number]) => string; - -export type BezierDemoOptions = { - [key in BezierCurveType]: { - disabled?: boolean; - inputOptions?: InputOption[]; - customPoints?: number[][]; - }; -}; - -export type InputOption = { - variable: string; - min?: number; - max?: number; - step?: number; - default?: number; - unit?: string | string[]; - inputType?: "slider" | "dropdown"; - options?: string[]; - disabled?: boolean; -}; -export type SubpathInputOption = InputOption & { - isDisabledForClosed?: boolean; -}; - -export function getCurveType(numPoints: number): BezierCurveType { - const mapping: Record = { - 2: "Linear", - 3: "Quadratic", - 4: "Cubic", - }; - - if (!(numPoints in mapping)) throw new Error("Invalid number of points for a bezier"); - - return mapping[numPoints]; -} - -export function getConstructorKey(bezierCurveType: BezierCurveType): WasmBezierConstructorKey { - const mapping: Record = { - Linear: "new_linear", - Quadratic: "new_quadratic", - Cubic: "new_cubic", - }; - return mapping[bezierCurveType]; -} - -export type DemoArgs = { - title: string; - disabled?: boolean; -}; - -export type BezierDemoArgs = { - points: number[][]; - inputOptions: InputOption[]; -} & DemoArgs; - -export type SubpathDemoArgs = { - triples: (number[] | undefined)[][]; - closed: boolean; -} & DemoArgs; - -export const BEZIER_T_VALUE_VARIANTS = ["Parametric", "Euclidean"] as const; -export const SUBPATH_T_VALUE_VARIANTS = ["GlobalParametric", "GlobalEuclidean"] as const; - -const CAP_VARIANTS = ["Butt", "Round", "Square"] as const; -const JOIN_VARIANTS = ["Bevel", "Miter", "Round"] as const; - -export const POINT_INDEX_TO_MANIPULATOR: WasmSubpathManipulatorKey[] = ["set_anchor", "set_in_handle", "set_out_handle"]; - -// Given the number of points in the curve, map the index of a point to the correct manipulator key -export const MANIPULATOR_KEYS_FROM_BEZIER_TYPE: { [key in BezierCurveType]: WasmBezierManipulatorKey[] } = { - Linear: ["set_start", "set_end"], - Quadratic: ["set_start", "set_handle_start", "set_end"], - Cubic: ["set_start", "set_handle_start", "set_handle_end", "set_end"], -}; - -export function getBezierDemoPointDefaults() { - // We use a function to generate a new object each time it is called - // to prevent one instance from being shared and modified across demos - return { - Linear: [ - [55, 60], - [165, 120], - ], - Quadratic: [ - [55, 50], - [165, 30], - [185, 170], - ], - Cubic: [ - [55, 30], - [85, 140], - [175, 30], - [185, 160], - ], - }; -} - -export function getSubpathDemoArgs(): SubpathDemoArgs[] { - // We use a function to generate a new object each time it is called - // to prevent one instance from being shared and modified across demos - return [ - { - title: "Open Subpath", - triples: [ - [[45, 20], undefined, [35, 90]], - [[175, 40], [85, 40], undefined], - [[200, 175], undefined, undefined], - [[125, 100], [65, 120], undefined], - ], - closed: false, - }, - { - title: "Closed Subpath", - triples: [ - [[60, 125], undefined, [65, 40]], - [[155, 30], [145, 120], undefined], - [ - [170, 150], - [200, 90], - [95, 185], - ], - ], - closed: true, - }, - ]; -} - -export const tSliderOptions = { - variable: "t", - inputType: "slider", - min: -0.01, - max: 1.01, - step: 0.01, - default: 0.5, -}; - -export const errorOptions = { - variable: "error", - inputType: "slider", - min: 0.1, - max: 2, - step: 0.1, - default: 0.5, -}; - -export const minimumSeparationOptions = { - variable: "minimum_separation", - inputType: "slider", - min: 0.001, - max: 0.25, - step: 0.001, - default: 0.05, -}; - -export const intersectionErrorOptions = { - variable: "error", - inputType: "slider", - min: 0.001, - max: 0.525, - step: 0.0025, - default: 0.02, -}; - -export const separationDiskDiameter = { - variable: "separation_disk_diameter", - inputType: "slider", - min: 2.5, - max: 25, - step: 0.1, - default: 5, -}; - -export const bezierTValueVariantOptions = { - variable: "TVariant", - inputType: "dropdown", - default: 0, - options: BEZIER_T_VALUE_VARIANTS, -}; - -export const subpathTValueVariantOptions = { - variable: "TVariant", - inputType: "dropdown", - default: 0, - options: SUBPATH_T_VALUE_VARIANTS, -}; - -export const joinOptions = { - variable: "join", - inputType: "dropdown", - default: 0, - options: JOIN_VARIANTS, -}; - -export const capOptions = { - variable: "cap", - inputType: "dropdown", - default: 0, - options: CAP_VARIANTS, -}; diff --git a/website/other/bezier-rs-demos/tsconfig.json b/website/other/bezier-rs-demos/tsconfig.json deleted file mode 100644 index 4d4d94dd71..0000000000 --- a/website/other/bezier-rs-demos/tsconfig.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "strict": true, - "importHelpers": true, - "moduleResolution": "node", - "experimentalDecorators": true, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "sourceMap": true, - "baseUrl": ".", - "paths": { - "@/*": ["src/*"] - }, - "lib": ["ESNext", "DOM", "DOM.Iterable", "ScriptHost"] - }, - "include": ["src/**/*.ts", "src/**/*.d.ts", "*.ts", "*.js", "*.cjs"], - "exclude": ["node_modules"], - "ts-node": { - "compilerOptions": { - "module": "commonjs", - "useDefineForClassFields": false, - "noImplicitOverride": true - } - } -} diff --git a/website/other/bezier-rs-demos/vite.config.ts b/website/other/bezier-rs-demos/vite.config.ts deleted file mode 100644 index a6a7dd4539..0000000000 --- a/website/other/bezier-rs-demos/vite.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* eslint-disable no-console */ - -import path from "path"; - -import { defineConfig } from "vite"; - -const projectRootDir = path.resolve(__dirname); - -// https://vitejs.dev/config/ -export default defineConfig({ - base: "", - resolve: { - alias: [{ find: "@", replacement: path.resolve(projectRootDir, "src") }], - }, - server: { - port: 8000, - host: "0.0.0.0", - }, -}); diff --git a/website/other/bezier-rs-demos/wasm/Cargo.toml b/website/other/bezier-rs-demos/wasm/Cargo.toml deleted file mode 100644 index ea98b1d3a4..0000000000 --- a/website/other/bezier-rs-demos/wasm/Cargo.toml +++ /dev/null @@ -1,50 +0,0 @@ -[package] -name = "bezier-rs-wasm" -publish = false -version = "0.0.0" -rust-version = "1.85" -authors = ["Graphite Authors "] -edition = "2024" -readme = "../../README.md" -homepage = "https://graphite.rs" -repository = "https://github.com/GraphiteEditor/Graphite" -license = "Apache-2.0" - -[lib] -crate-type = ["cdylib", "rlib"] - -[features] -default = [] -logging = ["log"] - -[dependencies] -# Workspace dependencies -bezier-rs = { workspace = true } -wasm-bindgen = { workspace = true } -js-sys = { workspace = true } -glam = { workspace = true } -log = { workspace = true, optional = true } - -[dev-dependencies] -log = { workspace = true } - -[package.metadata.wasm-pack.profile.dev] -wasm-opt = false - -[package.metadata.wasm-pack.profile.dev.wasm-bindgen] -debug-js-glue = true -demangle-name-section = true -dwarf-debug-info = false - -[package.metadata.wasm-pack.profile.release] -wasm-opt = ["-Oz", "--enable-bulk-memory"] - -[package.metadata.wasm-pack.profile.release.wasm-bindgen] -debug-js-glue = false -demangle-name-section = false -dwarf-debug-info = false - -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = [ - 'cfg(wasm_bindgen_unstable_test_coverage)', -] } diff --git a/website/other/bezier-rs-demos/wasm/src/bezier.rs b/website/other/bezier-rs-demos/wasm/src/bezier.rs deleted file mode 100644 index 50275b3c7a..0000000000 --- a/website/other/bezier-rs-demos/wasm/src/bezier.rs +++ /dev/null @@ -1,723 +0,0 @@ -use crate::svg_drawing::*; -use crate::utils::{parse_cap, parse_point}; -use bezier_rs::{ArcStrategy, ArcsOptions, Bezier, Identifier, TValue, TValueType}; -use glam::DVec2; -use js_sys::Array; -use wasm_bindgen::prelude::*; -use wasm_bindgen::{JsCast, JsValue}; - -#[wasm_bindgen] -pub enum WasmMaximizeArcs { - Automatic, // 0 - On, // 1 - Off, // 2 -} - -const SCALE_UNIT_VECTOR_FACTOR: f64 = 50.; - -/// Wrapper of the `Bezier` struct to be used in JS. -#[wasm_bindgen] -#[derive(Clone)] -pub struct WasmBezier(Bezier); - -fn convert_wasm_maximize_arcs(wasm_enum_value: WasmMaximizeArcs) -> ArcStrategy { - match wasm_enum_value { - WasmMaximizeArcs::Automatic => ArcStrategy::Automatic, - WasmMaximizeArcs::On => ArcStrategy::FavorLargerArcs, - WasmMaximizeArcs::Off => ArcStrategy::FavorCorrectness, - } -} - -fn parse_t_variant(t_variant: &String, t: f64) -> TValue { - match t_variant.as_str() { - "Parametric" => TValue::Parametric(t), - "Euclidean" => TValue::Euclidean(t), - _ => panic!("Unexpected TValue string: '{t_variant}'"), - } -} - -/// An empty id type for use in tests -#[derive(Clone, Copy, PartialEq, Eq, Hash)] -pub(crate) struct EmptyId; - -impl Identifier for EmptyId { - fn new() -> Self { - Self - } -} - -#[wasm_bindgen] -impl WasmBezier { - pub fn new_linear(js_points: JsValue) -> WasmBezier { - let array = js_points.dyn_into::().unwrap(); - let point1 = parse_point(&array.get(0)); - let point2 = parse_point(&array.get(1)); - WasmBezier(Bezier::from_linear_dvec2(point1, point2)) - } - - /// Expect js_points to be a list of 3 pairs. - pub fn new_quadratic(js_points: JsValue) -> WasmBezier { - let array = js_points.dyn_into::().unwrap(); - let point1 = parse_point(&array.get(0)); - let point2 = parse_point(&array.get(1)); - let point3 = parse_point(&array.get(2)); - WasmBezier(Bezier::from_quadratic_dvec2(point1, point2, point3)) - } - - /// Expect js_points to be a list of 4 pairs. - pub fn new_cubic(js_points: JsValue) -> WasmBezier { - let array = js_points.dyn_into::().unwrap(); - let point1 = parse_point(&array.get(0)); - let point2 = parse_point(&array.get(1)); - let point3 = parse_point(&array.get(2)); - let point4 = parse_point(&array.get(3)); - WasmBezier(Bezier::from_cubic_dvec2(point1, point2, point3, point4)) - } - - fn draw_bezier_through_points(bezier: Bezier, through_point: DVec2) -> String { - let mut bezier_string = String::new(); - bezier.to_svg( - &mut bezier_string, - CURVE_ATTRIBUTES.to_string(), - ANCHOR_ATTRIBUTES.to_string(), - HANDLE_ATTRIBUTES.to_string().replace(GRAY, RED), - HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, RED), - ); - let through_point_circle = format!(r#""#, through_point.x, through_point.y, ANCHOR_ATTRIBUTES); - - wrap_svg_tag(format!("{bezier_string}{through_point_circle}")) - } - - pub fn quadratic_through_points(js_points: JsValue, t: f64) -> String { - let array = js_points.dyn_into::().unwrap(); - let point1 = parse_point(&array.get(0)); - let point2 = parse_point(&array.get(1)); - let point3 = parse_point(&array.get(2)); - let bezier = Bezier::quadratic_through_points(point1, point2, point3, Some(t)); - WasmBezier::draw_bezier_through_points(bezier, point2) - } - - pub fn cubic_through_points(js_points: JsValue, t: f64, midpoint_separation: f64) -> String { - let array = js_points.dyn_into::().unwrap(); - let point1 = parse_point(&array.get(0)); - let point2 = parse_point(&array.get(1)); - let point3 = parse_point(&array.get(2)); - let bezier = Bezier::cubic_through_points(point1, point2, point3, Some(t), Some(midpoint_separation)); - WasmBezier::draw_bezier_through_points(bezier, point2) - } - - pub fn set_start(&mut self, x: f64, y: f64) { - self.0.set_start(DVec2::new(x, y)); - } - - pub fn set_end(&mut self, x: f64, y: f64) { - self.0.set_end(DVec2::new(x, y)); - } - - pub fn set_handle_start(&mut self, x: f64, y: f64) { - self.0.set_handle_start(DVec2::new(x, y)); - } - - pub fn set_handle_end(&mut self, x: f64, y: f64) { - self.0.set_handle_end(DVec2::new(x, y)); - } - - pub fn get_points(&self) -> JsValue { - JsValue::from( - self.0 - .get_points() - .map(|point| [JsValue::from_f64(point.x), JsValue::from_f64(point.y)].iter().collect::()) - .collect::(), - ) - } - - fn get_bezier_path(&self) -> String { - let mut bezier = String::new(); - self.0.to_svg( - &mut bezier, - CURVE_ATTRIBUTES.to_string(), - ANCHOR_ATTRIBUTES.to_string(), - HANDLE_ATTRIBUTES.to_string(), - HANDLE_LINE_ATTRIBUTES.to_string(), - ); - bezier - } - - pub fn to_svg(&self) -> String { - wrap_svg_tag(self.get_bezier_path()) - } - - pub fn length(&self) -> String { - let bezier = self.get_bezier_path(); - wrap_svg_tag(format!("{bezier}{}", draw_text(format!("Length: {:.2}", self.0.length(None)), TEXT_OFFSET_X, TEXT_OFFSET_Y, BLACK))) - } - - pub fn length_centroid(&self) -> String { - let bezier = self.get_bezier_path(); - let centroid = self.0.length_centroid(None); - let point_text = draw_circle(centroid, 4., RED, 1.5, WHITE); - wrap_svg_tag(format!("{bezier}{}", point_text)) - } - - pub fn evaluate(&self, raw_t: f64, t_variant: String) -> String { - let bezier = self.get_bezier_path(); - let t = parse_t_variant(&t_variant, raw_t); - let point = self.0.evaluate(t); - let content = format!("{bezier}{}", draw_circle(point, 4., RED, 1.5, WHITE)); - wrap_svg_tag(content) - } - - pub fn compute_lookup_table(&self, steps: usize, t_variant: String) -> String { - let bezier = self.get_bezier_path(); - let tvalue_type = match t_variant.as_str() { - "Parametric" => TValueType::Parametric, - "Euclidean" => TValueType::Euclidean, - _ => panic!("Unexpected TValue string: '{t_variant}'"), - }; - let table_values: Vec = self.0.compute_lookup_table(Some(steps), Some(tvalue_type)).collect(); - let circles: String = table_values - .iter() - .map(|point| draw_circle(*point, 3., RED, 1.5, WHITE)) - .fold("".to_string(), |acc, circle| acc + &circle); - let content = format!("{bezier}{circles}"); - wrap_svg_tag(content) - } - - pub fn derivative(&self) -> String { - let bezier = self.get_bezier_path(); - let derivative = self.0.derivative(); - if derivative.is_none() { - return bezier; - } - - let mut derivative_svg_path = String::new(); - derivative.unwrap().to_svg( - &mut derivative_svg_path, - CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), - ANCHOR_ATTRIBUTES.to_string().replace(BLACK, RED), - HANDLE_ATTRIBUTES.to_string().replace(GRAY, RED), - HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, RED), - ); - let content = format!("{bezier}{derivative_svg_path}"); - wrap_svg_tag(content) - } - - pub fn tangent(&self, raw_t: f64, t_variant: String) -> String { - let bezier = self.get_bezier_path(); - let t = parse_t_variant(&t_variant, raw_t); - - let tangent_point = self.0.tangent(t); - let intersection_point = self.0.evaluate(t); - let tangent_end = intersection_point + tangent_point * SCALE_UNIT_VECTOR_FACTOR; - - let content = format!( - "{bezier}{}{}{}", - draw_circle(intersection_point, 3., RED, 1., WHITE), - draw_line(intersection_point.x, intersection_point.y, tangent_end.x, tangent_end.y, RED, 1.), - draw_circle(tangent_end, 3., RED, 1., WHITE), - ); - wrap_svg_tag(content) - } - - pub fn normal(&self, raw_t: f64, t_variant: String) -> String { - let bezier = self.get_bezier_path(); - let t = parse_t_variant(&t_variant, raw_t); - - let normal_point = self.0.normal(t); - let intersection_point = self.0.evaluate(t); - let normal_end = intersection_point + normal_point * SCALE_UNIT_VECTOR_FACTOR; - - let content = format!( - "{bezier}{}{}{}", - draw_line(intersection_point.x, intersection_point.y, normal_end.x, normal_end.y, RED, 1.), - draw_circle(intersection_point, 3., RED, 1., WHITE), - draw_circle(normal_end, 3., RED, 1., WHITE), - ); - wrap_svg_tag(content) - } - - pub fn curvature(&self, raw_t: f64, t_variant: String) -> String { - let bezier = self.get_bezier_path(); - let t = parse_t_variant(&t_variant, raw_t); - - let intersection_point = self.0.evaluate(t); - let normal_point = self.0.normal(t); - let curvature = self.0.curvature(t); - let content = if curvature.abs() < 0.000001 { - // Linear curve segment: the radius is infinite so we don't draw it - format!("{bezier}{}", draw_circle(intersection_point, 3., RED, 1., WHITE)) - } else { - let radius = 1. / curvature; - let curvature_center = intersection_point + normal_point * radius; - - format!( - "{bezier}{}{}{}{}", - draw_circle(curvature_center, radius.abs(), RED, 1., NONE), - draw_line(intersection_point.x, intersection_point.y, curvature_center.x, curvature_center.y, RED, 1.), - draw_circle(intersection_point, 3., RED, 1., WHITE), - draw_circle(curvature_center, 3., RED, 1., WHITE), - ) - }; - wrap_svg_tag(content) - } - - pub fn split(&self, raw_t: f64, t_variant: String) -> String { - let t = parse_t_variant(&t_variant, raw_t); - let beziers: [Bezier; 2] = self.0.split(t); - - let mut bezier_svg_1 = String::new(); - beziers[0].to_svg( - &mut bezier_svg_1, - CURVE_ATTRIBUTES.to_string().replace(BLACK, ORANGE).replace("stroke-width=\"2\"", "stroke-width=\"8\"") + " opacity=\"0.5\"", - ANCHOR_ATTRIBUTES.to_string().replace(BLACK, ORANGE), - HANDLE_ATTRIBUTES.to_string().replace(GRAY, ORANGE), - HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, ORANGE), - ); - - let mut bezier_svg_2 = String::new(); - beziers[1].to_svg( - &mut bezier_svg_2, - CURVE_ATTRIBUTES.to_string().replace(BLACK, RED).replace("stroke-width=\"2\"", "stroke-width=\"8\"") + " opacity=\"0.5\"", - ANCHOR_ATTRIBUTES.to_string().replace(BLACK, RED), - HANDLE_ATTRIBUTES.to_string().replace(GRAY, RED), - HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, RED), - ); - - wrap_svg_tag(format!("{}{bezier_svg_1}{bezier_svg_2}", self.get_bezier_path())) - } - - pub fn trim(&self, raw_t1: f64, raw_t2: f64, t_variant: String) -> String { - let (t1, t2) = (parse_t_variant(&t_variant, raw_t1), parse_t_variant(&t_variant, raw_t2)); - let trimmed_bezier = self.0.trim(t1, t2); - - let mut trimmed_bezier_svg = String::new(); - trimmed_bezier.to_svg( - &mut trimmed_bezier_svg, - CURVE_ATTRIBUTES.to_string().replace(BLACK, RED).replace("stroke-width=\"2\"", "stroke-width=\"8\"") + " opacity=\"0.5\"", - ANCHOR_ATTRIBUTES.to_string().replace(BLACK, RED), - HANDLE_ATTRIBUTES.to_string().replace(GRAY, RED), - HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, RED), - ); - - wrap_svg_tag(format!("{}{trimmed_bezier_svg}", self.get_bezier_path())) - } - - pub fn project(&self, x: f64, y: f64) -> String { - let projected_t_value = self.0.project(DVec2::new(x, y)); - let projected_point = self.0.evaluate(TValue::Parametric(projected_t_value)); - - let bezier = self.get_bezier_path(); - let content = format!("{bezier}{}", draw_line(projected_point.x, projected_point.y, x, y, RED, 1.),); - wrap_svg_tag(content) - } - pub fn tangents_to_point(&self, x: f64, y: f64) -> String { - let bezier = self.get_bezier_path(); - let mut content = String::new(); - for t in self.0.tangents_to_point(DVec2::new(x, y)) { - let point = self.0.evaluate(TValue::Parametric(t)); - content += &draw_line(x, y, point.x, point.y, RED, 1.); - } - use std::fmt::Write; - write!(content, "{bezier}").unwrap(); - - wrap_svg_tag(content) - } - - pub fn normals_to_point(&self, x: f64, y: f64) -> String { - let bezier = self.get_bezier_path(); - let mut content = String::new(); - for t in self.0.normals_to_point(DVec2::new(x, y)) { - let point = self.0.evaluate(TValue::Parametric(t)); - content += &draw_line(x, y, point.x, point.y, RED, 1.); - } - use std::fmt::Write; - write!(content, "{bezier}").unwrap(); - - wrap_svg_tag(content) - } - - pub fn local_extrema(&self) -> String { - let local_extrema = self.0.local_extrema(); - - let bezier = self.get_bezier_path(); - let circles: String = local_extrema - .into_iter() - .zip([RED, GREEN]) - .flat_map(|(t_value_list, color)| { - t_value_list.map(move |t_value| { - let point = self.0.evaluate(TValue::Parametric(t_value)); - draw_circle(point, 3., color, 1.5, WHITE) - }) - }) - .fold("".to_string(), |acc, circle| acc + &circle); - - let content = format!( - "{bezier}{circles}{}{}", - draw_text("X extrema".to_string(), TEXT_OFFSET_X, TEXT_OFFSET_Y - 20., RED), - draw_text("Y extrema".to_string(), TEXT_OFFSET_X, TEXT_OFFSET_Y, GREEN), - ); - wrap_svg_tag(content) - } - - pub fn bounding_box(&self) -> String { - let [bbox_min_corner, bbox_max_corner] = self.0.bounding_box(); - - let bezier = self.get_bezier_path(); - let content = format!( - "{bezier}", - bbox_min_corner.x, - bbox_min_corner.y, - bbox_max_corner.x - bbox_min_corner.x, - bbox_max_corner.y - bbox_min_corner.y, - ); - wrap_svg_tag(content) - } - - pub fn inflections(&self) -> String { - let inflections: Vec = self.0.inflections(); - - let bezier = self.get_bezier_path(); - let circles: String = inflections - .iter() - .map(|&t_value| { - let point = self.0.evaluate(TValue::Parametric(t_value)); - draw_circle(point, 3., RED, 1.5, WHITE) - }) - .fold("".to_string(), |acc, circle| acc + &circle); - let content = format!("{bezier}{circles}"); - wrap_svg_tag(content) - } - - pub fn de_casteljau_points(&self, raw_t: f64, t_variant: String) -> String { - let t = parse_t_variant(&t_variant, raw_t); - let points: Vec> = self.0.de_casteljau_points(t); - - let bezier_svg = self.get_bezier_path(); - - let casteljau_svg = points - .iter() - .enumerate() - .map(|(index, points)| { - let color_light = format!("hsl({}, 100%, 50%)", 90 * index); - let points_and_handle_lines = points - .iter() - .enumerate() - .map(|(index, point)| { - let circle = draw_circle(*point, 3., &color_light, 1.5, WHITE); - if index != 0 { - let prev_point = points[index - 1]; - let line = draw_line(prev_point.x, prev_point.y, point.x, point.y, &color_light, 1.5); - - circle + line.as_str() - } else { - circle - } - }) - .fold("".to_string(), |acc, point_svg| acc + &point_svg); - points_and_handle_lines - }) - .fold("".to_string(), |acc, points_svg| acc + &points_svg); - let content = format!("{bezier_svg}{casteljau_svg}"); - wrap_svg_tag(content) - } - - pub fn rotate(&self, angle: f64, pivot_x: f64, pivot_y: f64) -> String { - let original_bezier_svg = self.get_bezier_path(); - let rotated_bezier = self.0.rotate_about_point(angle, DVec2::new(pivot_x, pivot_y)); - let mut rotated_bezier_svg = String::new(); - rotated_bezier.to_svg(&mut rotated_bezier_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new()); - let pivot = draw_circle(DVec2::new(pivot_x, pivot_y), 3., GRAY, 1.5, WHITE); - - // Line between pivot and start point on curve - let original_dashed_line = format!( - r#""#, - self.0.start().x, - self.0.start().y - ); - let rotated_dashed_line = format!( - r#""#, - rotated_bezier.start().x, - rotated_bezier.start().y - ); - - wrap_svg_tag(format!("{original_bezier_svg}{rotated_bezier_svg}{pivot}{original_dashed_line}{rotated_dashed_line}")) - } - - fn intersect(&self, curve: &Bezier, error: Option, minimum_separation: Option) -> Vec { - self.0.intersections(curve, error, minimum_separation) - } - - pub fn intersect_line_segment(&self, js_points: JsValue) -> String { - let array = js_points.dyn_into::().unwrap(); - let point1 = parse_point(&array.get(0)); - let point2 = parse_point(&array.get(1)); - let line = Bezier::from_linear_dvec2(point1, point2); - - let bezier_curve_svg = self.get_bezier_path(); - - let mut line_svg = String::new(); - line.to_svg(&mut line_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new()); - - let intersections_svg = self - .intersect(&line, None, None) - .iter() - .map(|intersection_t| { - let point = &self.0.evaluate(TValue::Parametric(*intersection_t)); - draw_circle(*point, 4., RED, 1.5, WHITE) - }) - .fold(String::new(), |acc, item| format!("{acc}{item}")); - wrap_svg_tag(format!("{bezier_curve_svg}{line_svg}{intersections_svg}")) - } - - pub fn intersect_quadratic_segment(&self, js_points: JsValue, error: f64, minimum_separation: f64) -> String { - let array = js_points.dyn_into::().unwrap(); - let point1 = parse_point(&array.get(0)); - let point2 = parse_point(&array.get(1)); - let point3 = parse_point(&array.get(2)); - let quadratic = Bezier::from_quadratic_dvec2(point1, point2, point3); - - let bezier_curve_svg = self.get_bezier_path(); - - let mut quadratic_svg = String::new(); - quadratic.to_svg(&mut quadratic_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new()); - - let intersections_svg = self - .intersect(&quadratic, Some(error), Some(minimum_separation)) - .iter() - .map(|intersection_t| { - let point = &self.0.evaluate(TValue::Parametric(*intersection_t)); - draw_circle(*point, 4., RED, 1.5, WHITE) - }) - .fold(String::new(), |acc, item| format!("{acc}{item}")); - wrap_svg_tag(format!("{bezier_curve_svg}{quadratic_svg}{intersections_svg}")) - } - - pub fn intersect_cubic_segment(&self, js_points: JsValue, error: f64, minimum_separation: f64) -> String { - let array = js_points.dyn_into::().unwrap(); - let point1 = parse_point(&array.get(0)); - let point2 = parse_point(&array.get(1)); - let point3 = parse_point(&array.get(2)); - let point4 = parse_point(&array.get(3)); - let cubic = Bezier::from_cubic_dvec2(point1, point2, point3, point4); - - let bezier_curve_svg = self.get_bezier_path(); - - let mut cubic_svg = String::new(); - cubic.to_svg(&mut cubic_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new()); - - let intersections_svg = self - .intersect(&cubic, Some(error), Some(minimum_separation)) - .iter() - .map(|intersection_t| { - let point = &self.0.evaluate(TValue::Parametric(*intersection_t)); - draw_circle(*point, 4., RED, 1.5, WHITE) - }) - .fold(String::new(), |acc, item| format!("{acc}{item}")); - - wrap_svg_tag(format!("{bezier_curve_svg}{cubic_svg}{intersections_svg}")) - } - - /// The wrapped return type is `Vec<[f64; 2]>`. - pub fn intersect_self(&self, error: f64, minimum_separation: f64) -> String { - let bezier_curve_svg = self.get_bezier_path(); - let intersect_self_svg = self - .0 - .self_intersections(Some(error), Some(minimum_separation)) - .iter() - .map(|intersection_t| { - let point = &self.0.evaluate(TValue::Parametric(intersection_t[0])); - draw_circle(*point, 4., RED, 1.5, WHITE) - }) - .fold(bezier_curve_svg, |acc, item| format!("{acc}{item}")); - - wrap_svg_tag(intersect_self_svg) - } - - pub fn intersect_rectangle(&self, js_points: JsValue) -> String { - let array = js_points.dyn_into::().unwrap(); - let point1 = parse_point(&array.get(0)); - let point2 = parse_point(&array.get(1)); - - let bezier_curve_svg = self.get_bezier_path(); - - let mut rectangle_svg = String::new(); - [ - Bezier::from_linear_coordinates(point1.x, point1.y, point2.x, point1.y), - Bezier::from_linear_coordinates(point2.x, point1.y, point2.x, point2.y), - Bezier::from_linear_coordinates(point2.x, point2.y, point1.x, point2.y), - Bezier::from_linear_coordinates(point1.x, point2.y, point1.x, point1.y), - ] - .iter() - .for_each(|line| line.to_svg(&mut rectangle_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new())); - - let intersections_svg = self - .0 - .rectangle_intersections(point1, point2) - .iter() - .map(|intersection_t| { - let point = &self.0.evaluate(TValue::Parametric(*intersection_t)); - draw_circle(*point, 4., RED, 1.5, WHITE) - }) - .fold(String::new(), |acc, item| format!("{acc}{item}")); - wrap_svg_tag(format!("{bezier_curve_svg}{rectangle_svg}{intersections_svg}")) - } - - pub fn reduce(&self) -> String { - let original_curve_svg = self.get_bezier_path(); - let bezier_curves_svg: String = self - .0 - .reduce(None) - .iter() - .enumerate() - .map(|(index, bezier_curve)| { - let mut curve_svg = String::new(); - bezier_curve.to_svg( - &mut curve_svg, - CURVE_ATTRIBUTES.to_string().replace(BLACK, &format!("hsl({}, 100%, 50%)", (40 * index))), - String::new(), - String::new(), - String::new(), - ); - curve_svg - }) - .fold(original_curve_svg, |acc, item| format!("{acc}{item}")); - wrap_svg_tag(bezier_curves_svg) - } - - pub fn offset(&self, distance: f64) -> String { - let original_curve_svg = self.get_bezier_path(); - let bezier_curves_svg = self - .0 - .offset::(distance) - .iter() - .enumerate() - .map(|(index, bezier_curve)| { - let mut curve_svg = String::new(); - bezier_curve.to_svg( - &mut curve_svg, - CURVE_ATTRIBUTES.to_string().replace(BLACK, &format!("hsl({}, 100%, 50%)", (40 * index))), - String::new(), - String::new(), - String::new(), - ); - curve_svg - }) - .fold(original_curve_svg, |acc, item| format!("{acc}{item}")); - wrap_svg_tag(bezier_curves_svg) - } - - pub fn outline(&self, distance: f64, cap: i32) -> String { - let cap = parse_cap(cap); - let outline_subpath = self.0.outline::(distance, cap); - if outline_subpath.is_empty() { - return String::new(); - } - - let mut outline_svg = String::new(); - outline_subpath.to_svg(&mut outline_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new()); - let bezier_svg = self.get_bezier_path(); - - wrap_svg_tag(format!("{bezier_svg}{outline_svg}")) - } - - pub fn graduated_outline(&self, start_distance: f64, end_distance: f64, cap: i32) -> String { - let cap = parse_cap(cap); - let outline_subpath = self.0.graduated_outline::(start_distance, end_distance, cap); - if outline_subpath.is_empty() { - return String::new(); - } - - let mut outline_svg = String::new(); - outline_subpath.to_svg(&mut outline_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new()); - let bezier_svg = self.get_bezier_path(); - - wrap_svg_tag(format!("{bezier_svg}{outline_svg}")) - } - - pub fn skewed_outline(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64, cap: i32) -> String { - let cap = parse_cap(cap); - let outline_subpath = self.0.skewed_outline::(distance1, distance2, distance3, distance4, cap); - if outline_subpath.is_empty() { - return String::new(); - } - - let mut outline_svg = String::new(); - outline_subpath.to_svg(&mut outline_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new()); - let bezier_svg = self.get_bezier_path(); - - wrap_svg_tag(format!("{bezier_svg}{outline_svg}")) - } - - pub fn arcs(&self, error: f64, max_iterations: usize, maximize_arcs: WasmMaximizeArcs) -> String { - let original_curve_svg = self.get_bezier_path(); - - // Get sectors - let strategy = convert_wasm_maximize_arcs(maximize_arcs); - let options = ArcsOptions { error, max_iterations, strategy }; - let arcs_svg = self - .0 - .arcs(options) - .iter() - .enumerate() - .map(|(idx, sector)| { - draw_sector( - sector.center, - sector.radius, - -sector.start_angle, - -sector.end_angle, - format!("hsl({}, 100%, 50%, 75%)", (40 * idx)).as_str(), - 1., - format!("hsl({}, 100%, 50%, 37.5%)", (40 * idx)).as_str(), - ) - }) - .fold(original_curve_svg, |acc, item| format!("{acc}{item}")); - wrap_svg_tag(arcs_svg) - } - - pub fn join(&self, js_points: &JsValue) -> String { - let array = js_points.to_owned().dyn_into::().unwrap(); - let other_bezier: Bezier = match self.0.get_points().count() { - 2 => { - let point1 = parse_point(&array.get(0)); - let point2 = parse_point(&array.get(1)); - Bezier::from_linear_dvec2(point1, point2) - } - 3 => { - let point1 = parse_point(&array.get(0)); - let point2 = parse_point(&array.get(1)); - let point3 = parse_point(&array.get(2)); - Bezier::from_quadratic_dvec2(point1, point2, point3) - } - 4 => { - let point1 = parse_point(&array.get(0)); - let point2 = parse_point(&array.get(1)); - let point3 = parse_point(&array.get(2)); - let point4 = parse_point(&array.get(3)); - Bezier::from_cubic_dvec2(point1, point2, point3, point4) - } - _ => unreachable!(), - }; - - let mut other_bezier_svg = String::new(); - other_bezier.to_svg( - &mut other_bezier_svg, - CURVE_ATTRIBUTES.to_string().replace(BLACK, GRAY), - ANCHOR_ATTRIBUTES.to_string().replace(BLACK, GRAY), - String::new(), - String::new(), - ); - - let joining_bezier: Bezier = self.0.join(&other_bezier); - let mut joining_bezier_svg = String::new(); - joining_bezier.to_svg( - &mut joining_bezier_svg, - CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), - ANCHOR_ATTRIBUTES.to_string().replace(BLACK, RED), - String::new(), - String::new(), - ); - - let bezier_svg = self.get_bezier_path(); - wrap_svg_tag(format!("{bezier_svg}{joining_bezier_svg}{other_bezier_svg}")) - } -} diff --git a/website/other/bezier-rs-demos/wasm/src/lib.rs b/website/other/bezier-rs-demos/wasm/src/lib.rs deleted file mode 100644 index 9dff90b260..0000000000 --- a/website/other/bezier-rs-demos/wasm/src/lib.rs +++ /dev/null @@ -1,71 +0,0 @@ -pub mod bezier; -pub mod subpath; -mod svg_drawing; -mod utils; - -use wasm_bindgen::prelude::*; - -#[cfg(feature = "logging")] -pub static LOGGER: WasmLog = WasmLog; -#[cfg(feature = "logging")] -thread_local! { pub static HAS_CRASHED: std::cell::RefCell = const { std::cell::RefCell::new(false) } } - -/// Initialize the backend -#[wasm_bindgen(start)] -pub fn init() { - #[cfg(feature = "logging")] - { - // Set up the logger with a default level of debug - log::set_logger(&LOGGER).expect("Failed to set logger"); - log::set_max_level(log::LevelFilter::Trace); - - fn panic_hook(info: &std::panic::PanicHookInfo<'_>) { - // Skip if we have already panicked - if HAS_CRASHED.with(|cell| cell.replace(true)) { - return; - } - log::error!("{}", info); - } - - std::panic::set_hook(Box::new(panic_hook)); - } -} - -/// Logging to the JS console -#[cfg(feature = "logging")] -#[wasm_bindgen] -extern "C" { - #[wasm_bindgen(js_namespace = console)] - fn log(msg: &str, format: &str); - #[wasm_bindgen(js_namespace = console)] - fn info(msg: &str, format: &str); - #[wasm_bindgen(js_namespace = console)] - fn warn(msg: &str, format: &str); - #[wasm_bindgen(js_namespace = console)] - fn error(msg: &str, format: &str); -} - -#[cfg(feature = "logging")] -#[derive(Default)] -pub struct WasmLog; - -#[cfg(feature = "logging")] -impl log::Log for WasmLog { - fn enabled(&self, metadata: &log::Metadata) -> bool { - metadata.level() <= log::Level::Info - } - - fn log(&self, record: &log::Record) { - let (log, name, color): (fn(&str, &str), &str, &str) = match record.level() { - log::Level::Trace => (log, "trace", "color:plum"), - log::Level::Debug => (log, "debug", "color:cyan"), - log::Level::Warn => (warn, "warn", "color:goldenrod"), - log::Level::Info => (info, "info", "color:mediumseagreen"), - log::Level::Error => (error, "error", "color:red"), - }; - let msg = &format!("%c{}\t{}", name, record.args()); - log(msg, color) - } - - fn flush(&self) {} -} diff --git a/website/other/bezier-rs-demos/wasm/src/subpath.rs b/website/other/bezier-rs-demos/wasm/src/subpath.rs deleted file mode 100644 index 54f87c6c43..0000000000 --- a/website/other/bezier-rs-demos/wasm/src/subpath.rs +++ /dev/null @@ -1,585 +0,0 @@ -use crate::svg_drawing::*; -use crate::utils::{parse_cap, parse_join, parse_point}; -use bezier_rs::{Bezier, ManipulatorGroup, Subpath, SubpathTValue, TValueType}; -use glam::DVec2; -use js_sys::Array; -use js_sys::Math; -use std::fmt::Write; -use wasm_bindgen::prelude::*; -use wasm_bindgen::{JsCast, JsValue}; - -#[derive(Clone, PartialEq, Hash)] -pub(crate) struct EmptyId; - -impl bezier_rs::Identifier for EmptyId { - fn new() -> Self { - Self - } -} - -/// Wrapper of the `Subpath` struct to be used in JS. -#[wasm_bindgen] -pub struct WasmSubpath(Subpath); - -const SCALE_UNIT_VECTOR_FACTOR: f64 = 50.; - -fn parse_t_variant(t_variant: &String, t: f64) -> SubpathTValue { - match t_variant.as_str() { - "GlobalParametric" => SubpathTValue::GlobalParametric(t), - "GlobalEuclidean" => SubpathTValue::GlobalEuclidean(t), - _ => panic!("Unexpected TValue string: '{t_variant}'"), - } -} - -#[wasm_bindgen] -impl WasmSubpath { - /// Expects js_points to be an unbounded list of triples, where each item is a tuple of floats. - /// The input TypeScript type is: (number[] | undefined)[][] - pub fn from_triples(js_points: JsValue, closed: bool) -> WasmSubpath { - let point_triples = js_points - .dyn_into::() - .unwrap() - .iter() - .map(|manipulator_group| { - let triple = manipulator_group.dyn_into::().unwrap(); - let anchor = parse_point(&triple.get(0)); - let in_handle = if triple.get(1).is_falsy() { None } else { Some(parse_point(&triple.get(1))) }; - let out_handle = if triple.get(2).is_falsy() { None } else { Some(parse_point(&triple.get(2))) }; - [Some(anchor), in_handle, out_handle] - }) - .collect::>(); - - let manipulator_groups = point_triples - .into_iter() - .map(|point_triple| ManipulatorGroup { - anchor: point_triple[0].unwrap(), - in_handle: point_triple[1], - out_handle: point_triple[2], - id: EmptyId, - }) - .collect(); - WasmSubpath(Subpath::new(manipulator_groups, closed)) - } - - pub fn set_anchor(&mut self, index: usize, x: f64, y: f64) { - self.0[index].anchor = DVec2::new(x, y); - } - - pub fn set_in_handle(&mut self, index: usize, x: f64, y: f64) { - self.0[index].in_handle = Some(DVec2::new(x, y)); - } - - pub fn set_out_handle(&mut self, index: usize, x: f64, y: f64) { - self.0[index].out_handle = Some(DVec2::new(x, y)); - } - - pub fn to_svg(&self) -> String { - format!("{}{}{}", SVG_OPEN_TAG, self.to_default_svg(), SVG_CLOSE_TAG) - } - - fn to_default_svg(&self) -> String { - let mut subpath_svg = String::new(); - self.0.to_svg( - &mut subpath_svg, - CURVE_ATTRIBUTES.to_string(), - ANCHOR_ATTRIBUTES.to_string(), - HANDLE_ATTRIBUTES.to_string(), - HANDLE_LINE_ATTRIBUTES.to_string(), - ); - subpath_svg - } - - fn to_filled_svg(&self) -> String { - let mut subpath_svg = String::new(); - self.0.to_svg( - &mut subpath_svg, - CURVE_FILLED_ATTRIBUTES.to_string(), - ANCHOR_ATTRIBUTES.to_string(), - HANDLE_ATTRIBUTES.to_string(), - HANDLE_LINE_ATTRIBUTES.to_string(), - ); - subpath_svg - } - - pub fn insert(&self, t: f64, t_variant: String) -> String { - let mut subpath = self.0.clone(); - let t = parse_t_variant(&t_variant, t); - - subpath.insert(t); - let point = self.0.evaluate(t); - - let point_text = draw_circle(point, 4., RED, 1.5, WHITE); - wrap_svg_tag(format!("{}{}", WasmSubpath(subpath).to_default_svg(), point_text)) - } - - pub fn length(&self) -> String { - let length_text = draw_text(format!("Length: {:.2}", self.0.length(None)), 5., 193., BLACK); - wrap_svg_tag(format!("{}{}", self.to_default_svg(), length_text)) - } - - pub fn length_centroid(&self) -> String { - let centroid = self.0.length_centroid(None, true).unwrap(); - let point_text = draw_circle(centroid, 4., RED, 1.5, WHITE); - wrap_svg_tag(format!("{}{}", self.to_default_svg(), point_text)) - } - - pub fn area(&self, error: f64, minimum_separation: f64) -> String { - let area_text = draw_text(format!("Area: {}", self.0.area(Some(error), Some(minimum_separation))), 5., 193., BLACK); - wrap_svg_tag(format!("{}{}", self.to_filled_svg(), area_text)) - } - - pub fn area_centroid(&self, error: f64, minimum_separation: f64) -> String { - let point_text = draw_circle(self.0.area_centroid(Some(error), Some(minimum_separation), None).unwrap(), 4., RED, 1.5, WHITE); - wrap_svg_tag(format!("{}{}", self.to_filled_svg(), point_text)) - } - - pub fn poisson_disk_points(&self, separation_disk_diameter: f64) -> String { - let r = separation_disk_diameter / 2.; - - let subpath_svg = self.to_default_svg(); - let points = self - .0 - .poisson_disk_points(separation_disk_diameter, Math::random, &[(self.0.clone(), self.0.bounding_box().unwrap())], 0); - - let points_style = format!(""); - let content = points - .iter() - .map(|point| format!("", point.x, point.y)) - .collect::>() - .join(""); - wrap_svg_tag(format!("{subpath_svg}{points_style}{content}")) - } - - pub fn evaluate(&self, t: f64, t_variant: String) -> String { - let t = parse_t_variant(&t_variant, t); - let point = self.0.evaluate(t); - - let point_text = draw_circle(point, 4., RED, 1.5, WHITE); - wrap_svg_tag(format!("{}{}", self.to_default_svg(), point_text)) - } - - pub fn compute_lookup_table(&self, steps: usize, t_variant: String) -> String { - let subpath = self.to_default_svg(); - let tvalue_type = match t_variant.as_str() { - "GlobalParametric" => TValueType::Parametric, - "GlobalEuclidean" => TValueType::Euclidean, - _ => panic!("Unexpected TValue string: '{t_variant}'"), - }; - let table_values: Vec = self.0.compute_lookup_table(Some(steps), Some(tvalue_type)); - let circles: String = table_values - .iter() - .map(|point| draw_circle(*point, 3., RED, 1.5, WHITE)) - .fold("".to_string(), |acc, circle| acc + &circle); - let content = format!("{subpath}{circles}"); - wrap_svg_tag(content) - } - - pub fn tangent(&self, t: f64, t_variant: String) -> String { - let t = parse_t_variant(&t_variant, t); - - let intersection_point = self.0.evaluate(t); - let tangent_point = self.0.tangent(t); - let tangent_end = intersection_point + tangent_point * SCALE_UNIT_VECTOR_FACTOR; - - let point_text = draw_circle(intersection_point, 4., RED, 1.5, WHITE); - let line_text = draw_line(intersection_point.x, intersection_point.y, tangent_end.x, tangent_end.y, RED, 1.); - let tangent_end_point = draw_circle(tangent_end, 3., RED, 1., WHITE); - wrap_svg_tag(format!("{}{}{}{}", self.to_default_svg(), point_text, line_text, tangent_end_point)) - } - - pub fn normal(&self, t: f64, t_variant: String) -> String { - let t = parse_t_variant(&t_variant, t); - - let intersection_point = self.0.evaluate(t); - let normal_point = self.0.normal(t); - let normal_end = intersection_point + normal_point * SCALE_UNIT_VECTOR_FACTOR; - - let point_text = draw_circle(intersection_point, 4., RED, 1.5, WHITE); - let line_text = draw_line(intersection_point.x, intersection_point.y, normal_end.x, normal_end.y, RED, 1.); - let normal_end_point = draw_circle(normal_end, 3., RED, 1., WHITE); - wrap_svg_tag(format!("{}{}{}{}", self.to_default_svg(), point_text, line_text, normal_end_point)) - } - - pub fn local_extrema(&self) -> String { - let local_extrema: [Vec; 2] = self.0.local_extrema(); - - let bezier = self.to_default_svg(); - let circles: String = local_extrema - .iter() - .zip([RED, GREEN]) - .flat_map(|(t_value_list, color)| { - t_value_list.iter().map(|&t_value| { - let point = self.0.evaluate(SubpathTValue::GlobalParametric(t_value)); - draw_circle(point, 3., color, 1.5, WHITE) - }) - }) - .fold("".to_string(), |acc, circle| acc + &circle); - - let content = format!( - "{bezier}{circles}{}{}", - draw_text("X extrema".to_string(), TEXT_OFFSET_X, TEXT_OFFSET_Y - 20., RED), - draw_text("Y extrema".to_string(), TEXT_OFFSET_X, TEXT_OFFSET_Y, GREEN), - ); - wrap_svg_tag(content) - } - - pub fn bounding_box(&self) -> String { - let subpath_svg = self.to_default_svg(); - let bounding_box = self.0.bounding_box(); - match bounding_box { - None => wrap_svg_tag(subpath_svg), - Some(bounding_box) => { - let content = format!( - "{subpath_svg}", - bounding_box[0].x, - bounding_box[0].y, - bounding_box[1].x - bounding_box[0].x, - bounding_box[1].y - bounding_box[0].y, - ); - wrap_svg_tag(content) - } - } - } - - pub fn inflections(&self) -> String { - let inflections: Vec = self.0.inflections(); - - let bezier = self.to_default_svg(); - let circles: String = inflections - .iter() - .map(|&t_value| { - let point = self.0.evaluate(SubpathTValue::GlobalParametric(t_value)); - draw_circle(point, 3., RED, 1.5, WHITE) - }) - .fold("".to_string(), |acc, circle| acc + &circle); - let content = format!("{bezier}{circles}"); - wrap_svg_tag(content) - } - - pub fn rotate(&self, angle: f64, pivot_x: f64, pivot_y: f64) -> String { - let subpath_svg = self.to_default_svg(); - let rotated_subpath = self.0.rotate_about_point(angle, DVec2::new(pivot_x, pivot_y)); - let mut rotated_subpath_svg = String::new(); - rotated_subpath.to_svg(&mut rotated_subpath_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new()); - let pivot = draw_circle(DVec2::new(pivot_x, pivot_y), 3., GRAY, 1.5, WHITE); - - // Line between pivot and start point on curve - let original_dashed_line = format!( - r#""#, - self.0.iter().next().unwrap().start().x, - self.0.iter().next().unwrap().start().y - ); - let rotated_dashed_line = format!( - r#""#, - rotated_subpath.iter().next().unwrap().start().x, - rotated_subpath.iter().next().unwrap().start().y - ); - - wrap_svg_tag(format!("{subpath_svg}{rotated_subpath_svg}{pivot}{original_dashed_line}{rotated_dashed_line}")) - } - - pub fn project(&self, x: f64, y: f64) -> String { - let (segment_index, projected_t) = self.0.project(DVec2::new(x, y)).unwrap(); - let projected_point = self.0.evaluate(SubpathTValue::Parametric { segment_index, t: projected_t }); - - let subpath_svg = self.to_default_svg(); - let content = format!("{subpath_svg}{}", draw_line(projected_point.x, projected_point.y, x, y, RED, 1.),); - wrap_svg_tag(content) - } - - pub fn intersect_line_segment(&self, js_points: JsValue, error: f64, minimum_separation: f64) -> String { - let array = js_points.dyn_into::().unwrap(); - let point1 = parse_point(&array.get(0)); - let point2 = parse_point(&array.get(1)); - let line = Bezier::from_linear_dvec2(point1, point2); - - let subpath_svg = self.to_default_svg(); - - let empty_string = String::new(); - let mut line_svg = String::new(); - line.to_svg( - &mut line_svg, - CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), - empty_string.clone(), - empty_string.clone(), - empty_string, - ); - - let intersections_svg = self - .0 - .intersections(&line, Some(error), Some(minimum_separation)) - .iter() - .map(|(segment_index, intersection_t)| { - let point = self.0.evaluate(SubpathTValue::Parametric { - segment_index: *segment_index, - t: *intersection_t, - }); - draw_circle(point, 4., RED, 1.5, WHITE) - }) - .fold(String::new(), |acc, item| format!("{acc}{item}")); - - wrap_svg_tag(format!("{subpath_svg}{line_svg}{intersections_svg}")) - } - - pub fn intersect_quadratic_segment(&self, js_points: JsValue, error: f64, minimum_separation: f64) -> String { - let array = js_points.dyn_into::().unwrap(); - let point1 = parse_point(&array.get(0)); - let point2 = parse_point(&array.get(1)); - let point3 = parse_point(&array.get(2)); - let line = Bezier::from_quadratic_dvec2(point1, point2, point3); - - let subpath_svg = self.to_default_svg(); - - let empty_string = String::new(); - let mut line_svg = String::new(); - line.to_svg( - &mut line_svg, - CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), - empty_string.clone(), - empty_string.clone(), - empty_string, - ); - - let intersections_svg = self - .0 - .intersections(&line, Some(error), Some(minimum_separation)) - .iter() - .map(|(segment_index, intersection_t)| { - let point = self.0.evaluate(SubpathTValue::Parametric { - segment_index: *segment_index, - t: *intersection_t, - }); - draw_circle(point, 4., RED, 1.5, WHITE) - }) - .fold(String::new(), |acc, item| format!("{acc}{item}")); - - wrap_svg_tag(format!("{subpath_svg}{line_svg}{intersections_svg}")) - } - - pub fn intersect_cubic_segment(&self, js_points: JsValue, error: f64, minimum_separation: f64) -> String { - let array = js_points.dyn_into::().unwrap(); - let point1 = parse_point(&array.get(0)); - let point2 = parse_point(&array.get(1)); - let point3 = parse_point(&array.get(2)); - let point4 = parse_point(&array.get(3)); - let line = Bezier::from_cubic_dvec2(point1, point2, point3, point4); - - let subpath_svg = self.to_default_svg(); - - let empty_string = String::new(); - let mut line_svg = String::new(); - line.to_svg( - &mut line_svg, - CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), - empty_string.clone(), - empty_string.clone(), - empty_string, - ); - - let intersections_svg = self - .0 - .intersections(&line, Some(error), Some(minimum_separation)) - .iter() - .map(|(segment_index, intersection_t)| { - let point = self.0.evaluate(SubpathTValue::Parametric { - segment_index: *segment_index, - t: *intersection_t, - }); - draw_circle(point, 4., RED, 1.5, WHITE) - }) - .fold(String::new(), |acc, item| format!("{acc}{item}")); - - wrap_svg_tag(format!("{subpath_svg}{line_svg}{intersections_svg}")) - } - - pub fn self_intersections(&self, error: f64, minimum_separation: f64) -> String { - let subpath_svg = self.to_default_svg(); - let self_intersections_svg = self - .0 - .self_intersections(Some(error), Some(minimum_separation)) - .iter() - .map(|(segment_index, intersection_t)| { - let point = self.0.evaluate(SubpathTValue::Parametric { - segment_index: *segment_index, - t: *intersection_t, - }); - draw_circle(point, 4., RED, 1.5, WHITE) - }) - .fold(String::new(), |acc, item| format!("{acc}{item}")); - - wrap_svg_tag(format!("{subpath_svg}{self_intersections_svg}")) - } - - pub fn intersect_rectangle(&self, js_points: JsValue, error: f64, minimum_separation: f64) -> String { - let array = js_points.dyn_into::().unwrap(); - let point1 = parse_point(&array.get(0)); - let point2 = parse_point(&array.get(1)); - - let subpath_svg = self.to_default_svg(); - - let mut rectangle_svg = String::new(); - [ - Bezier::from_linear_coordinates(point1.x, point1.y, point2.x, point1.y), - Bezier::from_linear_coordinates(point2.x, point1.y, point2.x, point2.y), - Bezier::from_linear_coordinates(point2.x, point2.y, point1.x, point2.y), - Bezier::from_linear_coordinates(point1.x, point2.y, point1.x, point1.y), - ] - .iter() - .for_each(|line| line.to_svg(&mut rectangle_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new())); - - let intersections_svg = self - .0 - .rectangle_intersections(point1, point2, Some(error), Some(minimum_separation)) - .iter() - .map(|(segment_index, intersection_t)| { - let point = self.0.evaluate(SubpathTValue::Parametric { - segment_index: *segment_index, - t: *intersection_t, - }); - draw_circle(point, 4., RED, 1.5, WHITE) - }) - .fold(String::new(), |acc, item| format!("{acc}{item}")); - - wrap_svg_tag(format!("{subpath_svg}{rectangle_svg}{intersections_svg}")) - } - - pub fn inside_subpath(&self, js_points: JsValue, error: f64, minimum_separation: f64) -> String { - let array = js_points.dyn_into::().unwrap(); - let points = array.iter().map(|p| parse_point(&p)); - let other = Subpath::::from_anchors(points, true); - - let is_inside = self.0.is_inside_subpath(&other, Some(error), Some(minimum_separation)); - let color = if is_inside { RED } else { BLACK }; - - let self_svg = self.to_default_svg(); - let mut other_svg = String::new(); - other.curve_to_svg(&mut other_svg, CURVE_ATTRIBUTES.replace(BLACK, color)); - - wrap_svg_tag(format!("{self_svg}{other_svg}")) - } - - pub fn curvature(&self, t: f64, t_variant: String) -> String { - let subpath = self.to_default_svg(); - let t = parse_t_variant(&t_variant, t); - - let intersection_point = self.0.evaluate(t); - let normal_point = self.0.normal(t); - let curvature = self.0.curvature(t); - let content = if curvature.abs() < 0.000001 { - // Linear curve segment: the radius is infinite so we don't draw it - format!("{subpath}{}", draw_circle(intersection_point, 3., RED, 1., WHITE)) - } else { - let radius = 1. / curvature; - let curvature_center = intersection_point + normal_point * radius; - - format!( - "{subpath}{}{}{}{}", - draw_circle(curvature_center, radius.abs(), RED, 1., NONE), - draw_line(intersection_point.x, intersection_point.y, curvature_center.x, curvature_center.y, RED, 1.), - draw_circle(intersection_point, 3., RED, 1., WHITE), - draw_circle(curvature_center, 3., RED, 1., WHITE), - ) - }; - wrap_svg_tag(content) - } - - pub fn split(&self, t: f64, t_variant: String) -> String { - let t = parse_t_variant(&t_variant, t); - let (main_subpath, optional_subpath) = self.0.split(t); - - let mut main_subpath_svg = String::new(); - let mut other_subpath_svg = String::new(); - if optional_subpath.is_some() { - main_subpath.to_svg( - &mut main_subpath_svg, - CURVE_ATTRIBUTES.to_string().replace(BLACK, ORANGE).replace("stroke-width=\"2\"", "stroke-width=\"8\"") + " opacity=\"0.5\"", - ANCHOR_ATTRIBUTES.to_string().replace(BLACK, ORANGE), - HANDLE_ATTRIBUTES.to_string().replace(GRAY, ORANGE), - HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, ORANGE), - ); - } else { - main_subpath.iter().enumerate().for_each(|(index, bezier)| { - let hue1 = &format!("hsla({}, 100%, 50%, 0.5)", 40 * index); - let hue2 = &format!("hsla({}, 100%, 50%, 0.5)", 40 * (index + 1)); - let gradient_id = &format!("gradient{index}"); - let start = bezier.start(); - let end = bezier.end(); - let _ = write!( - main_subpath_svg, - r#""#, - gradient_id, - start.x / 2., - start.y / 2., - end.x / 2., - end.y / 2., - hue1, - hue2 - ); - - let stroke = &format!("url(#{gradient_id})"); - bezier.curve_to_svg( - &mut main_subpath_svg, - CURVE_ATTRIBUTES.to_string().replace(BLACK, stroke).replace("stroke-width=\"2\"", "stroke-width=\"8\""), - ); - bezier.anchors_to_svg(&mut main_subpath_svg, ANCHOR_ATTRIBUTES.to_string().replace(BLACK, hue1)); - bezier.handles_to_svg(&mut main_subpath_svg, HANDLE_ATTRIBUTES.to_string().replace(GRAY, hue1)); - bezier.handle_lines_to_svg(&mut main_subpath_svg, HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, hue1)); - }); - } - - if let Some(subpath) = optional_subpath { - subpath.to_svg( - &mut other_subpath_svg, - CURVE_ATTRIBUTES.to_string().replace(BLACK, RED).replace("stroke-width=\"2\"", "stroke-width=\"8\"") + " opacity=\"0.5\"", - ANCHOR_ATTRIBUTES.to_string().replace(BLACK, RED), - HANDLE_ATTRIBUTES.to_string().replace(GRAY, RED), - HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, RED), - ); - } - - wrap_svg_tag(format!("{}{}{}", self.to_default_svg(), main_subpath_svg, other_subpath_svg)) - } - - pub fn trim(&self, t1: f64, t2: f64, t_variant: String) -> String { - let t1 = parse_t_variant(&t_variant, t1); - let t2 = parse_t_variant(&t_variant, t2); - let trimmed_subpath = self.0.trim(t1, t2); - - let mut trimmed_subpath_svg = String::new(); - trimmed_subpath.to_svg( - &mut trimmed_subpath_svg, - CURVE_ATTRIBUTES.to_string().replace(BLACK, RED).replace("stroke-width=\"2\"", "stroke-width=\"8\"") + " opacity=\"0.5\"", - ANCHOR_ATTRIBUTES.to_string().replace(BLACK, RED), - HANDLE_ATTRIBUTES.to_string().replace(GRAY, RED), - HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, RED), - ); - - wrap_svg_tag(format!("{}{}", self.to_default_svg(), trimmed_subpath_svg)) - } - - pub fn offset(&self, distance: f64, join: i32, miter_limit: f64) -> String { - let join = parse_join(join, miter_limit); - let offset_subpath = self.0.offset(distance, join); - - let mut offset_svg = String::new(); - offset_subpath.to_svg(&mut offset_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new()); - - wrap_svg_tag(format!("{}{offset_svg}", self.to_default_svg())) - } - - pub fn outline(&self, distance: f64, join: i32, cap: i32, miter_limit: f64) -> String { - let join = parse_join(join, miter_limit); - let cap = parse_cap(cap); - let (outline_piece1, outline_piece2) = self.0.outline(distance, join, cap); - - let mut outline_piece1_svg = String::new(); - outline_piece1.to_svg(&mut outline_piece1_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new()); - - let mut outline_piece2_svg = String::new(); - if let Some(outline) = outline_piece2 { - outline.to_svg(&mut outline_piece2_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new()); - } - - wrap_svg_tag(format!("{}{outline_piece1_svg}{outline_piece2_svg}", self.to_default_svg())) - } -} diff --git a/website/other/bezier-rs-demos/wasm/src/svg_drawing.rs b/website/other/bezier-rs-demos/wasm/src/svg_drawing.rs deleted file mode 100644 index a54c8403b4..0000000000 --- a/website/other/bezier-rs-demos/wasm/src/svg_drawing.rs +++ /dev/null @@ -1,69 +0,0 @@ -use glam::DVec2; - -// SVG drawing constants -pub const SVG_OPEN_TAG: &str = r#""#; -pub const SVG_CLOSE_TAG: &str = ""; - -// Stylistic constants -pub const BLACK: &str = "black"; -pub const WHITE: &str = "white"; -pub const GRAY: &str = "gray"; -pub const RED: &str = "red"; -pub const ORANGE: &str = "orange"; -// pub const PINK: &str = "pink"; -pub const GREEN: &str = "green"; -pub const NONE: &str = "none"; - -// Default attributes -pub const CURVE_ATTRIBUTES: &str = "stroke=\"black\" stroke-width=\"2\" fill=\"none\""; -pub const CURVE_FILLED_ATTRIBUTES: &str = "stroke=\"black\" stroke-width=\"2\" fill=\"lightgray\""; -pub const HANDLE_LINE_ATTRIBUTES: &str = "stroke=\"gray\" stroke-width=\"1\" fill=\"none\""; -pub const ANCHOR_ATTRIBUTES: &str = "r=\"4\" stroke=\"black\" stroke-width=\"2\" fill=\"white\""; -pub const HANDLE_ATTRIBUTES: &str = "r=\"3\" stroke=\"gray\" stroke-width=\"1.5\" fill=\"white\""; - -// Text constants -pub const TEXT_OFFSET_X: f64 = 5.; -pub const TEXT_OFFSET_Y: f64 = 193.; - -pub fn wrap_svg_tag(contents: String) -> String { - format!("{SVG_OPEN_TAG}{contents}{SVG_CLOSE_TAG}") -} - -/// Helper function to create an SVG text entity. -pub fn draw_text(text: String, x_pos: f64, y_pos: f64, fill: &str) -> String { - format!(r#"{text}"#) -} - -/// Helper function to create an SVG circle entity. -pub fn draw_circle(position: DVec2, radius: f64, stroke: &str, stroke_width: f64, fill: &str) -> String { - format!( - r#""#, - position.x, position.y - ) -} - -/// Helper function to create an SVG circle entity. -pub fn draw_line(start_x: f64, start_y: f64, end_x: f64, end_y: f64, stroke: &str, stroke_width: f64) -> String { - format!(r#""#) -} - -// Helper function to convert polar to cartesian coordinates -fn polar_to_cartesian(center_x: f64, center_y: f64, radius: f64, angle_in_rad: f64) -> [f64; 2] { - let x = center_x + radius * angle_in_rad.cos(); - let y = center_y + radius * -angle_in_rad.sin(); - [x, y] -} - -// Helper function to create an SVG drawing of a sector -pub fn draw_sector(center: DVec2, radius: f64, start_angle: f64, end_angle: f64, stroke: &str, stroke_width: f64, fill: &str) -> String { - let [start_x, start_y] = polar_to_cartesian(center.x, center.y, radius, start_angle); - let [end_x, end_y] = polar_to_cartesian(center.x, center.y, radius, end_angle); - // draw sector with fill color - let sector_svg = format!( - r#""#, - center.x, center.y - ); - // draw arc with stroke color - let arc_svg = format!(r#""#); - format!("{sector_svg}{arc_svg}") -} diff --git a/website/other/bezier-rs-demos/wasm/src/utils.rs b/website/other/bezier-rs-demos/wasm/src/utils.rs deleted file mode 100644 index 1a33b14a1d..0000000000 --- a/website/other/bezier-rs-demos/wasm/src/utils.rs +++ /dev/null @@ -1,29 +0,0 @@ -use bezier_rs::{Cap, Join}; -use glam::DVec2; -use js_sys::Array; -use wasm_bindgen::{JsCast, JsValue}; - -pub fn parse_join(join: i32, miter_limit: f64) -> Join { - match join { - 0 => Join::Bevel, - 1 => Join::Miter(Some(miter_limit)), - 2 => Join::Round, - _ => panic!("Unexpected Join value: '{join}'"), - } -} - -pub fn parse_cap(cap: i32) -> Cap { - match cap { - 0 => Cap::Butt, - 1 => Cap::Round, - 2 => Cap::Square, - _ => panic!("Unexpected Cap value: '{cap}'"), - } -} - -pub fn parse_point(js_point: &JsValue) -> DVec2 { - let point = js_point.to_owned().dyn_into::().unwrap(); - let x = point.get(0).as_f64().unwrap(); - let y = point.get(1).as_f64().unwrap(); - DVec2::new(x, y) -} diff --git a/website/other/build.sh b/website/other/build.sh deleted file mode 100644 index 3ee7e7af93..0000000000 --- a/website/other/build.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/sh -set -e # Exit with nonzero exit code if any individual command fails throughout the script - -echo 📁 Create output directory in 'website/other/dist' -cd website/other -mkdir dist - -echo 🔧 Install the latest Rust -curl https://sh.rustup.rs -sSf | sh -s -- -y -export PATH=$PATH:/opt/buildhome/.cargo/bin -rustup update stable -echo rustc version: -rustc --version - -echo 📦 Install wasm-pack -cargo install wasm-pack -echo wasm-pack version: -wasm-pack --version - -echo 🚧 Print installed node and npm versions -echo node version: -node --version -echo npm version: -npm --version - -echo 👷 Build Bezier-rs demos to 'website/other/dist/libraries/bezier-rs' -mkdir dist/libraries -mkdir dist/libraries/bezier-rs -cd bezier-rs-demos -npm ci -NODE_ENV=production npm run build -cp ../../static/fonts/common.css dist/fonts.css -mv dist/* ../dist/libraries/bezier-rs -cd .. diff --git a/website/package.json b/website/package.json index 429df6552f..b4151622aa 100644 --- a/website/package.json +++ b/website/package.json @@ -10,7 +10,8 @@ "license": "Apache-2.0", "homepage": "https://graphite.rs", "scripts": { - "install-fonts": "npm ci && node install-fonts.js" + "install-fonts": "npm ci && node build-scripts/install-fonts.js", + "generate-editor-structure": "node build-scripts/generate-editor-structure.js ../hierarchical_message_system_tree.txt ../hierarchical_message_system_tree.html" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^8.7.0", diff --git a/website/templates/macros/replacements.html b/website/templates/macros/replacements.html index 619dc29d7e..f34f76ca82 100644 --- a/website/templates/macros/replacements.html +++ b/website/templates/macros/replacements.html @@ -40,16 +40,12 @@

{{ article.title } {% endmacro text_balancer %} {% macro hierarchical_message_system_tree() %} -{%- set content = load_data(path = "other/editor-structure/replacement.html", format = "plain", required = false) -%} +{%- set content = load_data(path = "../../hierarchical_message_system_tree.html", format = "plain", required = false) -%} {%- set fallback = "
THIS CONTENT IS FILLED IN WHEN CI BUILDS THE WEBSITE.
 
-TO TEST IT LOCALLY, RUN:
-
-cd website/other/editor-structure
-
-AND THEN:
+TO TEST IT LOCALLY, FROM THE `website` DIRECTORY, RUN:
 
 cargo test --package graphite-editor --lib -- messages::message::test::generate_message_tree
-node generate.js ../../../hierarchical_message_system_tree.txt replacement.html
" -%} +npm run generate-editor-structure" -%} {{ content | default(value = fallback) | safe }} {% endmacro hierarchical_message_system_tree %}