Remove the deprecated/archived Bezier-rs library from the repo (#3058)

Remove the Bezier-rs library from the repo
This commit is contained in:
Keavon Chambers 2025-08-16 17:29:00 -07:00 committed by GitHub
parent d22b2ca927
commit 3bcec37493
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
60 changed files with 24 additions and 17053 deletions

View File

@ -63,14 +63,14 @@ jobs:
mkdir artifacts mkdir artifacts
mv hierarchical_message_system_tree.txt artifacts/hierarchical_message_system_tree.txt 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: | run: |
mv artifacts/* website/other/editor-structure mv artifacts/* .
- name: 🔧 Build auto-generated code docs artifacts into HTML - name: 🔧 Build auto-generated code docs artifacts into HTML
run: | run: |
cd website/other/editor-structure cd website
node generate.js hierarchical_message_system_tree.txt replacement.html npm run generate-editor-structure
- name: 🌐 Build Graphite website with Zola - name: 🌐 Build Graphite website with Zola
env: env:
@ -80,38 +80,6 @@ jobs:
npm run install-fonts npm run install-fonts
zola --config config.toml build --minify 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 - name: 📤 Publish to Cloudflare Pages
id: cloudflare id: cloudflare
uses: cloudflare/pages-action@1 uses: cloudflare/pages-action@1

1
.gitignore vendored
View File

@ -8,3 +8,4 @@ flamegraph.svg
.idea/ .idea/
.direnv .direnv
hierarchical_message_system_tree.txt hierarchical_message_system_tree.txt
hierarchical_message_system_tree.html

View File

@ -35,7 +35,7 @@
"rust-analyzer.cargo.allTargets": false, "rust-analyzer.cargo.allTargets": false,
// ESLint config // ESLint config
"eslint.format.enable": true, "eslint.format.enable": true,
"eslint.workingDirectories": ["./frontend", "./website/other/bezier-rs-demos", "./website"], "eslint.workingDirectories": ["./frontend", "./website"],
"eslint.validate": ["javascript", "typescript", "svelte"], "eslint.validate": ["javascript", "typescript", "svelte"],
// Svelte config // Svelte config
"svelte.plugin.svelte.compilerWarnings": { "svelte.plugin.svelte.compilerWarnings": {

22
Cargo.lock generated
View File

@ -461,28 +461,6 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 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]] [[package]]
name = "bincode" name = "bincode"
version = "1.3.3" version = "1.3.3"

View File

@ -20,9 +20,7 @@ members = [
"node-graph/preprocessor", "node-graph/preprocessor",
"libraries/dyn-any", "libraries/dyn-any",
"libraries/path-bool", "libraries/path-bool",
"libraries/bezier-rs",
"libraries/math-parser", "libraries/math-parser",
"website/other/bezier-rs-demos/wasm",
] ]
default-members = [ default-members = [
"editor", "editor",
@ -44,7 +42,6 @@ resolver = "2"
[workspace.dependencies] [workspace.dependencies]
# Local dependencies # Local dependencies
bezier-rs = { path = "libraries/bezier-rs", features = ["dyn-any", "serde"] }
dyn-any = { path = "libraries/dyn-any", features = [ dyn-any = { path = "libraries/dyn-any", features = [
"derive", "derive",
"glam", "glam",

View File

@ -1,29 +0,0 @@
[package]
name = "bezier-rs"
version = "0.4.0"
rust-version = "1.85"
edition = "2024"
authors = ["Graphite Authors <contact@graphite.rs>"]
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 }

View File

@ -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.

View File

@ -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.

View File

@ -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)

View File

@ -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.
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#bezier/constructor/solo" title="Constructor Demo"></iframe>
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.
/// <iframe frameBorder="0" width="100%" height="375px" src="https://graphite.rs/libraries/bezier-rs#bezier/bezier-through-points/solo" title="Through Points Demo"></iframe>
pub fn quadratic_through_points(start: DVec2, point_on_curve: DVec2, end: DVec2, t: Option<f64>) -> 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<f64>, midpoint_separation: Option<f64>) -> 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<String> {
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#"<path d="{SVG_ARG_MOVE}{} {} {}" {}/>"#, 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#"<path d="{}" {}/>"#, 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#"<circle cx="{}" cy="{}" {attributes}/><circle cx="{}" cy="{}" {attributes}/>"#,
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#"<circle cx="{}" cy="{}" {attributes}/>"#, handle.x, handle.y);
} else if let BezierHandles::Cubic { handle_start, handle_end } = self.handles {
let _ = write!(
svg,
r#"<circle cx="{}" cy="{}" {attributes}/><circle cx="{}" cy="{}" {attributes}/>"#,
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::<Vec<DVec2>>();
let other_points = b.get_points().collect::<Vec<DVec2>>();
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));
}
}

View File

@ -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: <https://pomax.github.io/bezierinfo/#explanation>.
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]`.
/// <iframe frameBorder="0" width="100%" height="350px" src="https://graphite.rs/libraries/bezier-rs#bezier/evaluate/solo" title="Evaluate Demo"></iframe>
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.
/// <iframe frameBorder="0" width="100%" height="350px" src="https://graphite.rs/libraries/bezier-rs#bezier/lookup-table/solo" title="Lookup-Table Demo"></iframe>
pub fn compute_lookup_table(&self, steps: Option<usize>, tvalue_type: Option<TValueType>) -> impl Iterator<Item = DVec2> + '_ {
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.
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#bezier/length/solo" title="Length Demo"></iframe>
pub fn length(&self, tolerance: Option<f64>) -> 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<f64>) -> (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.
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#bezier/length-centroid/solo" title="Length Centroid Demo"></iframe>
pub fn length_centroid(&self, tolerance: Option<f64>) -> 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.
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#bezier/project/solo" title="Project Demo"></iframe>
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::<Vec<_>>();
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::<Vec<_>>();
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.)))));
}
}

View File

@ -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<DVec2> {
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<DVec2> {
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<Item = DVec2> + 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),
}
}
}

View File

@ -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<H: std::hash::Hasher>(&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<DVec2> {
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<DVec2> {
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;
}

File diff suppressed because it is too large Load Diff

View File

@ -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.,
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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<DVec2>, b: Vec<DVec2>, 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<PointId: crate::Identifier>(subpath1: &Subpath<PointId>, subpath2: &Subpath<PointId>) -> bool {
subpath1.len() == subpath2.len() && subpath1.closed() == subpath2.closed() && subpath1.iter().eq(subpath2.iter())
}

View File

@ -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";

View File

@ -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};

View File

@ -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"
/// <https://scholarsarchive.byu.edu/facpub/237/>
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<DVec2> {
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, B>(a: A, b: B) -> impl Iterator<Item = (A::Item, B::Item)>
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<ActiveSquare>,
/// 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<Item = ActiveSquare>) {
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<Item = DVec2> {
// 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<Item = DVec2> {
// 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<PointsList>,
}
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<Item = DVec2> {
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<DVec2> {
self.cells.iter().flat_map(|cell| cell.list_cell()).collect()
}
}

View File

@ -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<const N: usize> {
coefficients: [f64; N],
}
impl<const N: usize> Polynomial<N> {
/// 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<N> {
Polynomial { coefficients }
}
/// Create a polynomial where all its coefficients are zero.
pub fn zero() -> Polynomial<N> {
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<const M: usize>(&self) -> Option<Polynomial<M>> {
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<N> {
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<Polynomial<N>> {
let mut ans = *self;
ans.antiderivative_mut()?;
Some(ans)
}
}
impl<const N: usize> Default for Polynomial<N> {
fn default() -> Self {
Self::zero()
}
}
impl<const N: usize> Display for Polynomial<N> {
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<const N: usize> AddAssign<&Polynomial<N>> for Polynomial<N> {
fn add_assign(&mut self, rhs: &Polynomial<N>) {
self.coefficients.iter_mut().zip(rhs.coefficients.iter()).for_each(|(a, b)| *a += b);
}
}
impl<const N: usize> Add for &Polynomial<N> {
type Output = Polynomial<N>;
fn add(self, other: &Polynomial<N>) -> Polynomial<N> {
let mut output = *self;
output += other;
output
}
}
impl<const N: usize> Neg for &Polynomial<N> {
type Output = Polynomial<N>;
fn neg(self) -> Polynomial<N> {
let mut output = *self;
output.coefficients.iter_mut().for_each(|x| *x = -*x);
output
}
}
impl<const N: usize> Neg for Polynomial<N> {
type Output = Polynomial<N>;
fn neg(mut self) -> Polynomial<N> {
self.coefficients.iter_mut().for_each(|x| *x = -*x);
self
}
}
impl<const N: usize> SubAssign<&Polynomial<N>> for Polynomial<N> {
fn sub_assign(&mut self, rhs: &Polynomial<N>) {
self.coefficients.iter_mut().zip(rhs.coefficients.iter()).for_each(|(a, b)| *a -= b);
}
}
impl<const N: usize> Sub for &Polynomial<N> {
type Output = Polynomial<N>;
fn sub(self, other: &Polynomial<N>) -> Polynomial<N> {
let mut output = *self;
output -= other;
output
}
}
impl<const N: usize> MulAssign<&Polynomial<N>> for Polynomial<N> {
fn mul_assign(&mut self, rhs: &Polynomial<N>) {
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<const N: usize> Mul for &Polynomial<N> {
type Output = Polynomial<N>;
fn mul(self, other: &Polynomial<N>) -> Polynomial<N> {
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");
}
}

View File

@ -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<PointId: crate::Identifier> Subpath<PointId> {
/// 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<ManipulatorGroup<PointId>>, 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<ManipulatorGroup<PointId>> = 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::<Vec<ManipulatorGroup<PointId>>>();
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<Bezier> {
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<PointId>] {
&self.manipulator_groups
}
/// Returns a mutable reference to the [ManipulatorGroup]s in the `Subpath`.
pub fn manipulator_groups_mut(&mut self) -> &mut Vec<ManipulatorGroup<PointId>> {
&mut self.manipulator_groups
}
/// Returns a vector of all the anchors (DVec2) for this `Subpath`.
pub fn anchors(&self) -> Vec<DVec2> {
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<String> = self.iter().map(|bezier| bezier.svg_curve_argument()).collect();
if self.closed {
curve_arguments.push(String::from(SVG_ARG_CLOSED));
}
let _ = write!(svg, r#"<path d="{} {}" {attributes}/>"#, 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<String> = self.iter().filter_map(|bezier| bezier.svg_handle_line_argument()).collect();
let _ = write!(svg, r#"<path d="{}" {attributes}/>"#, 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#"<circle cx="{}" cy="{}" {attributes}/>"#, point.anchor.x, point.anchor.y))
.collect::<Vec<String>>();
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#"<circle cx="{}" cy="{}" {attributes}/>"#, handle.x, handle.y))
.collect::<Vec<String>>();
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<Item = DVec2>, 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<Item = DVec2>, 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<ManipulatorGroup<PointId>> {
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 <https://mathworld.wolfram.com/CubicSpline.html>.
pub fn new_cubic_spline(points: Vec<DVec2>) -> 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<DVec2> {
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<DVec2> {
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::<EmptyId>::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}"
);
}
}
}

View File

@ -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<PointId: crate::Identifier> Subpath<PointId> {
/// 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.
/// <iframe frameBorder="0" width="100%" height="350px" src="https://graphite.rs/libraries/bezier-rs#subpath/lookup-table/solo" title="Lookup-Table Demo"></iframe>
pub fn compute_lookup_table(&self, steps: Option<usize>, tvalue_type: Option<TValueType>) -> Vec<DVec2> {
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.
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#subpath/length/solo" title="Length Demo"></iframe>
pub fn length(&self, tolerance: Option<f64>) -> 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<f64>, 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.
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#subpath/length-centroid/solo" title="Length Centroid Demo"></iframe>
pub fn length_centroid(&self, tolerance: Option<f64>, always_closed: bool) -> Option<DVec2> {
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<f64>, minimum_separation: Option<f64>) -> 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<f64>, minimum_separation: Option<f64>) -> 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.
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#subpath/area-centroid/solo" title="Area Centroid Demo"></iframe>
pub fn area_centroid(&self, error: Option<f64>, minimum_separation: Option<f64>, tolerance: Option<f64>) -> Option<DVec2> {
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::<Vec<f64>>();
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::<Vec<f64>>();
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.
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#subpath/project/solo" title="Project Demo"></iframe>
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<EmptyId> = 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);
}
}

View File

@ -1,231 +0,0 @@
use super::*;
use crate::consts::MAX_ABSOLUTE_DIFFERENCE;
use crate::utils::f64_compare;
use crate::{SubpathTValue, TValue};
impl<PointId: crate::Identifier> Subpath<PointId> {
/// 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<PointId>> {
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<PointId>> {
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<usize> {
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<PointId>) {
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<PointId>) {
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<PointId>> {
self.manipulator_groups.last_mut()
}
/// Remove a manipulator group at an index.
pub fn remove_manipulator_group(&mut self, index: usize) -> ManipulatorGroup<PointId> {
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<EmptyId> {
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<EmptyId> {
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);
}
}

View File

@ -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<PointId: crate::Identifier> {
manipulator_groups: Vec<ManipulatorGroup<PointId>>,
pub closed: bool,
}
#[cfg(feature = "dyn-any")]
unsafe impl<PointId: crate::Identifier> dyn_any::StaticType for Subpath<PointId> {
type Static = Subpath<PointId>;
}
/// 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<PointId>,
is_always_closed: bool,
}
impl<PointId: crate::Identifier> Index<usize> for Subpath<PointId> {
type Output = ManipulatorGroup<PointId>;
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<PointId: crate::Identifier> IndexMut<usize> for Subpath<PointId> {
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<PointId: crate::Identifier> 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<Self::Item> {
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<PointId: crate::Identifier> Debug for Subpath<PointId> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
f.debug_struct("Subpath").field("closed", &self.closed).field("manipulator_groups", &self.manipulator_groups).finish()
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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<PointId: crate::Identifier> {
pub anchor: DVec2,
pub in_handle: Option<DVec2>,
pub out_handle: Option<DVec2>,
pub id: PointId,
}
// TODO: Remove once we no longer need to hash floats in Graphite
impl<PointId: crate::Identifier> Hash for ManipulatorGroup<PointId> {
fn hash<H: core::hash::Hasher>(&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<PointId: crate::Identifier> dyn_any::StaticType for ManipulatorGroup<PointId> {
type Static = ManipulatorGroup<PointId>;
}
impl<PointId: crate::Identifier> Debug for ManipulatorGroup<PointId> {
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<PointId: crate::Identifier> ManipulatorGroup<PointId> {
/// Construct a new manipulator group from an anchor, in handle and out handle
pub fn new(anchor: DVec2, in_handle: Option<DVec2>, out_handle: Option<DVec2>) -> 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<DVec2>, out_handle: Option<DVec2>, 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<PointId>) -> 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,
}

File diff suppressed because it is too large Load Diff

View File

@ -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: <https://www.w3.org/TR/SVG2/painting.html#LineJoin>.
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<f64>),
/// 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: <https://www.w3.org/TR/SVG2/painting.html#LineCaps>.
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<f64>; 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<f64>; 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<f64>; 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<DVec2> {
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<PointId: crate::Identifier>(left: DVec2, arc_point: DVec2, right: DVec2, center: DVec2, angle: Option<f64>) -> (DVec2, ManipulatorGroup<PointId>, 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<f64>, b: Vec<f64>, 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<f64>; 3]) -> Vec<f64> {
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.));
}
}

View File

@ -864,7 +864,7 @@ async fn offset_path(_: impl Ctx, content: Table<Vector>, distance: f64, join: S
for mut bezpath in bezpaths { for mut bezpath in bezpaths {
bezpath.apply_affine(transform); 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( let mut bezpath_out = offset_bezpath(
&bezpath, &bezpath,
-distance, -distance,

View File

@ -59,7 +59,7 @@ impl std::hash::Hash for Vector {
} }
impl 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<Subpath<PointId>>, preserve_id: bool) { pub fn append_subpath(&mut self, subpath: impl Borrow<Subpath<PointId>>, preserve_id: bool) {
let subpath: &Subpath<PointId> = subpath.borrow(); let subpath: &Subpath<PointId> = subpath.borrow();
let stroke_id = StrokeId::ZERO; let stroke_id = StrokeId::ZERO;
@ -131,7 +131,7 @@ impl Vector {
self.point_domain.push(id, point.position); 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<Subpath<PointId>>) -> Self { pub fn from_subpath(subpath: impl Borrow<Subpath<PointId>>) -> Self {
Self::from_subpaths([subpath], false) Self::from_subpaths([subpath], false)
} }
@ -143,7 +143,7 @@ impl Vector {
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<Item = impl Borrow<Subpath<PointId>>>, preserve_id: bool) -> Self { pub fn from_subpaths(subpaths: impl IntoIterator<Item = impl Borrow<Subpath<PointId>>>, preserve_id: bool) -> Self {
let mut vector = Self::default(); let mut vector = Self::default();

View File

@ -1,5 +1,3 @@
//! requires bezier-rs
use crate::curve::{Curve, CurveManipulatorGroup, ValueMapperNode}; use crate::curve::{Curve, CurveManipulatorGroup, ValueMapperNode};
use graphene_core::color::{Channel, Linear}; use graphene_core::color::{Channel, Linear};
use graphene_core::context::Ctx; use graphene_core::context::Ctx;
@ -34,7 +32,7 @@ fn generate_curves<C: Channel + Linear>(_: impl Ctx, curve: Curve, #[implementat
pathseg_find_tvalues_for_x(segment, x) pathseg_find_tvalues_for_x(segment, x)
.next() .next()
.map(|t| segment.eval(t.clamp(0., 1.)).y) .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) .unwrap_or_else(|| (x - x0) / (x3 - x0) * (y3 - y0) + y0)
}; };
lut[index] = C::from_f64(y); lut[index] = C::from_f64(y);

1
website/.gitignore vendored
View File

@ -3,4 +3,3 @@ public/
static/fonts/ static/fonts/
static/syntax-highlighting.css static/syntax-highlighting.css
static/text-balancer.js static/text-balancer.js
other/editor-structure/replacement.html

View File

@ -95,7 +95,7 @@ const outputFile = process.argv[3];
if (!inputFile || !outputFile) { if (!inputFile || !outputFile) {
console.error("Error: Please provide the input text and output HTML file paths as arguments."); console.error("Error: Please provide the input text and output HTML file paths as arguments.");
console.log("Usage: node generate.js <input> <output>"); console.log("Usage: node generate-editor-structure.js <input txt> <output html>");
process.exit(1); process.exit(1);
} }

View File

@ -8,15 +8,15 @@ const basePath = path.resolve(__dirname);
// Define files to copy as [source, destination] pairs // Define files to copy as [source, destination] pairs
// Files with the same destination will be concatenated // Files with the same destination will be concatenated
const FILES_TO_COPY = [ const FILES_TO_COPY = [
["node_modules/@fontsource-variable/inter/opsz.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-variable/inter/opsz-italic.css", "../static/fonts/common.css"],
["node_modules/@fontsource/bona-nova/700.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 // Define directories to copy recursively as [source, destination] pairs
const DIRECTORIES_TO_COPY = [ const DIRECTORIES_TO_COPY = [
["node_modules/@fontsource-variable/inter/files", "static/fonts/files"], ["../node_modules/@fontsource-variable/inter/files", "../static/fonts/files"],
["node_modules/@fontsource/bona-nova/files", "static/fonts/files"], ["../node_modules/@fontsource/bona-nova/files", "../static/fonts/files"],
]; ];
// Track processed destination files and CSS content // 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 // 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 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..."); console.log("\nDownloading text-balancer.js...");
https https
.get(textBalancerUrl, (res) => { .get(textBalancerUrl, (res) => {

View File

@ -335,7 +335,7 @@ Duration: 9 months
Students: Hannah Li, Rob Nadal, Thomas Cheng, Linda Zheng, Jackie Chen Students: Hannah Li, Rob Nadal, Thomas Cheng, Linda Zheng, Jackie Chen
- [Bezier-rs library](https://crates.io/crates/bezier-rs) - [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/). **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/).

View File

@ -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,
},
],
},
};

View File

@ -1,5 +0,0 @@
/node_modules
!/public
/public/build
/dist
/wasm/pkg

View File

@ -1,6 +0,0 @@
{
"singleQuote": false,
"useTabs": true,
"tabWidth": 4,
"printWidth": 200
}

View File

@ -1,18 +0,0 @@
# Bezier-rs interactive documentation
Open these interactive docs: <https://graphite.rs/libraries/bezier-rs/>
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
```

View File

@ -1,13 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Bezier-rs Interactive Documentation</title>
<link rel="stylesheet" href="./style.css" />
<link href="fonts.css" rel="stylesheet" />
</head>
<body>
<noscript>JavaScript is required</noscript>
<script type="module" src="src/main.ts"></script>
</body>
</html>

View File

@ -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.");
}

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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;
}

View File

@ -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, number>): string => bezier.to_svg(),
},
"bezier-through-points": {
name: "Bezier Through Points",
callback: (bezier: WasmBezierInstance, options: Record<string, number>): 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, number>): string => bezier.length(),
},
"length-centroid": {
name: "Length Centroid",
callback: (bezier: WasmBezierInstance, _: Record<string, number>): string => bezier.length_centroid(),
},
evaluate: {
name: "Evaluate",
callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: 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<string, number>, _: 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, number>): 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<string, number>, _: 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<string, number>, 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<string, number>, _: 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<string, number>, 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<string, number>, _: 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<string, number>, _: 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<string, number>, _: 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<string, number>, 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, number>): 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, number>): string => bezier.bounding_box(),
},
inflections: {
name: "Inflections",
callback: (bezier: WasmBezierInstance, _: Record<string, number>): 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, number>): 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, number>): 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, number>): 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, number>): 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, number>): 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, number>): 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, number>): 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, number>): 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, number>): 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<string, number>, _: 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<BezierDemoOptions>;
triggerOnMouseMove?: boolean;
};
export default bezierFeatures as Record<BezierFeatureKey, BezierFeatureOptions>;

View File

@ -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<string, number>, _: 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<string, number>, _: undefined): string => subpath.area(options.error, options.minimum_separation),
inputOptions: [intersectionErrorOptions, minimumSeparationOptions],
},
"area-centroid": {
name: "Area Centroid",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: 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<string, number>, _: undefined): string => subpath.poisson_disk_points(options.separation_disk_diameter),
inputOptions: [separationDiskDiameter],
},
evaluate: {
name: "Evaluate",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: 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<string, number>, _: 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<string, number>, mouseLocation?: [number, number]): string =>
mouseLocation ? subpath.project(mouseLocation[0], mouseLocation[1]) : subpath.to_svg(),
triggerOnMouseMove: true,
},
tangent: {
name: "Tangent",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined): string => subpath.tangent(options.t, SUBPATH_T_VALUE_VARIANTS[options.TVariant]),
inputOptions: [subpathTValueVariantOptions, tSliderOptions],
},
normal: {
name: "Normal",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: 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, number>): 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, number>): 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, number>): 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, number>): string => subpath.self_intersections(options.error, options.minimum_separation),
inputOptions: [intersectionErrorOptions, minimumSeparationOptions],
},
"intersect-rectangle": {
name: "Intersect (Rectangle)",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): 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, number>): 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<string, number>, _: 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<string, number>, _: undefined): string => subpath.split(options.t, SUBPATH_T_VALUE_VARIANTS[options.TVariant]),
inputOptions: [subpathTValueVariantOptions, tSliderOptions],
},
trim: {
name: "Trim",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: 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, number>): 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, number>): 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, number>): 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<SubpathFeatureKey, SubpathFeatureOptions>;

View File

@ -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 = `<div id="bezier-demos"></div>`;
const container = document.getElementById("bezier-demos");
if (!container) return;
const key = splitHash[1];
const value = (bezierFeatures as Record<string, BezierFeatureOptions>)[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 = `<div id="subpath-demos"></div>`;
const container = document.getElementById("subpath-demos");
if (!container) return;
const key = splitHash[1];
const value = (subpathFeatures as Record<string, SubpathFeatureOptions>)[key];
if (value) container.append(subpathDemoGroup(key as SubpathFeatureKey, value));
return;
}
window.document.body.innerHTML = `
<h1 class="website-header">Bezier-rs Interactive Documentation</h1>
<p class="website-description">
This is the interactive documentation for the <a href="https://crates.io/crates/bezier-rs">Bezier-rs</a> library. View the
<a href="https://docs.rs/bezier-rs/latest/bezier_rs">crate documentation</a>
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.
</p>
<h2 class="category-header">Beziers</h2>
<div id="bezier-demos"></div>
<h2 class="category-header">Subpaths</h2>
<div id="subpath-demos"></div>
`.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<T extends DemoArgs>(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 `
<h3 class="demo-group-header">
<a href="#${id}">#</a>
${name}
</h3>
`.trim();
})()}
<div class="demo-row" data-demo-row></div>
`.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",
`
<h4 class="demo-header">${demo.title}</h4>
<div class="demo-figure" data-demo-figure></div>
<div class="parent-input-container" data-parent-input-container>
${(() =>
demo.inputOptions
.map((inputOption) =>
`
<div
class="${(() => {
if (inputOption.inputType === "dropdown") return "select-container";
if (inputOption.inputType === "slider") return "slider-container";
return "";
})()}"
data-input-container
>
<div class="input-label" data-input-label>
${inputOption.variable}: ${inputOption.inputType === "dropdown" ? "" : demo.sliderData[inputOption.variable]}${getSliderUnit(demo, inputOption.variable)}
</div>
${(() => {
if (inputOption.inputType !== "dropdown") return "";
return `
<select class="select-input" value="${inputOption.default}" ${inputOption.disabled ? "disabled" : ""} data-select>
${inputOption.options?.map((value, idx) => `<option value="${idx}" id="${idx}-${value}">${value}</option>`).join("\n")}
</select>
`.trim();
})()}
${(() => {
if (inputOption.inputType !== "slider") return "";
const ratio = (Number(inputOption.default) - (inputOption.min || 0)) / ((inputOption.max || 100) - (inputOption.min || 0));
return `
<input
class="slider-input"
type="range"
max="${inputOption.max}"
min="${inputOption.min}"
step="${inputOption.step}"
value="${inputOption.default}"
style="--range-ratio: ${ratio}"
data-slider-input
/>
`.trim();
})()}
</div>
`.trim(),
)
.join("\n"))()}
</div>
`.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);
});
}
});
}

View File

@ -1,241 +0,0 @@
import type * as WasmPkg from "@/../wasm/pkg";
type WasmRawInstance = typeof WasmPkg;
export type WasmBezierInstance = InstanceType<WasmRawInstance["WasmBezier"]>;
export type WasmSubpathInstance = InstanceType<WasmRawInstance["WasmSubpath"]>;
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<string, number>;
sliderUnits: Record<string, string | string[]>;
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<string, number>, mouseLocation?: [number, number]) => string;
export type SubpathCallback = (subpath: WasmSubpathInstance, options: Record<string, number>, 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<number, BezierCurveType> = {
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<BezierCurveType, WasmBezierConstructorKey> = {
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,
};

View File

@ -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
}
}
}

View File

@ -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",
},
});

View File

@ -1,50 +0,0 @@
[package]
name = "bezier-rs-wasm"
publish = false
version = "0.0.0"
rust-version = "1.85"
authors = ["Graphite Authors <contact@graphite.rs>"]
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)',
] }

View File

@ -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::<Array>().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::<Array>().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::<Array>().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#"<circle cx="{}" cy="{}" {}/>"#, 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::<Array>().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::<Array>().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::<Array>())
.collect::<Array>(),
)
}
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<DVec2> = 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}<rect x={} y ={} width=\"{}\" height=\"{}\" style=\"fill:{NONE};stroke:{RED};stroke-width:1\" />",
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<f64> = 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<Vec<DVec2>> = 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#"<line x1="{pivot_x}" y1="{pivot_y}" x2="{}" y2="{}" stroke="{ORANGE}" stroke-dasharray="0, 4" stroke-width="2" stroke-linecap="round"/>"#,
self.0.start().x,
self.0.start().y
);
let rotated_dashed_line = format!(
r#"<line x1="{pivot_x}" y1="{pivot_y}" x2="{}" y2="{}" stroke="{ORANGE}" stroke-dasharray="0, 4" stroke-width="2" stroke-linecap="round"/>"#,
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<f64>, minimum_separation: Option<f64>) -> Vec<f64> {
self.0.intersections(curve, error, minimum_separation)
}
pub fn intersect_line_segment(&self, js_points: JsValue) -> String {
let array = js_points.dyn_into::<Array>().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::<Array>().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::<Array>().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::<Array>().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::<EmptyId>(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::<EmptyId>(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::<EmptyId>(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::<EmptyId>(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::<Array>().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}"))
}
}

View File

@ -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<bool> = 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) {}
}

View File

@ -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<EmptyId>);
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::<Array>()
.unwrap()
.iter()
.map(|manipulator_group| {
let triple = manipulator_group.dyn_into::<Array>().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::<Vec<_>>();
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!("<style class=\"poisson\">style.poisson ~ circle {{ fill: {RED}; opacity: 0.25; }}</style>");
let content = points
.iter()
.map(|point| format!("<circle cx=\"{}\" cy=\"{}\" r=\"{r}\" />", point.x, point.y))
.collect::<Vec<_>>()
.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<DVec2> = 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<f64>; 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}<rect x={} y={} width=\"{}\" height=\"{}\" style=\"fill:{NONE};stroke:{RED};stroke-width:1\" />",
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<f64> = 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#"<line x1="{pivot_x}" y1="{pivot_y}" x2="{}" y2="{}" stroke="{ORANGE}" stroke-dasharray="0, 4" stroke-width="2" stroke-linecap="round"/>"#,
self.0.iter().next().unwrap().start().x,
self.0.iter().next().unwrap().start().y
);
let rotated_dashed_line = format!(
r#"<line x1="{pivot_x}" y1="{pivot_y}" x2="{}" y2="{}" stroke="{ORANGE}" stroke-dasharray="0, 4" stroke-width="2" stroke-linecap="round"/>"#,
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::<Array>().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::<Array>().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::<Array>().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::<Array>().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::<Array>().unwrap();
let points = array.iter().map(|p| parse_point(&p));
let other = Subpath::<EmptyId>::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#"<defs><linearGradient id="{}" x1="{}%" y1="{}%" x2="{}%" y2="{}%"><stop offset="0%" stop-color="{}"/><stop offset="100%" stop-color="{}"/></linearGradient></defs>"#,
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()))
}
}

View File

@ -1,69 +0,0 @@
use glam::DVec2;
// SVG drawing constants
pub const SVG_OPEN_TAG: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" width="250px" height="200px">"#;
pub const SVG_CLOSE_TAG: &str = "</svg>";
// 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 x="{x_pos}" y="{y_pos}" fill="{fill}" font-family="monospace">{text}</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#"<circle cx="{}" cy="{}" r="{radius}" stroke="{stroke}" stroke-width="{stroke_width}" fill="{fill}"/>"#,
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#"<line x1="{start_x}" y1="{start_y}" x2="{end_x}" y2="{end_y}" stroke="{stroke}" stroke-width="{stroke_width}"/>"#)
}
// 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#"<path d="M {start_x} {start_y} A {radius} {radius} 0 0 1 {end_x} {end_y} L {} {} L {start_x} {start_y} Z" stroke="none" fill="{fill}" />"#,
center.x, center.y
);
// draw arc with stroke color
let arc_svg = format!(r#"<path d="M {start_x} {start_y} A {radius} {radius} 0 0 1 {end_x} {end_y}" stroke="{stroke}" stroke-width="{stroke_width}" fill="none"/>"#);
format!("{sector_svg}{arc_svg}")
}

View File

@ -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::<Array>().unwrap();
let x = point.get(0).as_f64().unwrap();
let y = point.get(1).as_f64().unwrap();
DVec2::new(x, y)
}

View File

@ -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 ..

View File

@ -10,7 +10,8 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"homepage": "https://graphite.rs", "homepage": "https://graphite.rs",
"scripts": { "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": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.7.0", "@typescript-eslint/eslint-plugin": "^8.7.0",

View File

@ -40,16 +40,12 @@
{% endmacro text_balancer %} {% endmacro text_balancer %}
{% macro hierarchical_message_system_tree() %} {% 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 = "<pre>THIS CONTENT IS FILLED IN WHEN CI BUILDS THE WEBSITE. {%- set fallback = "<pre>THIS CONTENT IS FILLED IN WHEN CI BUILDS THE WEBSITE.
TO TEST IT LOCALLY, RUN: TO TEST IT LOCALLY, FROM THE `website` DIRECTORY, RUN:
cd website/other/editor-structure
AND THEN:
cargo test --package graphite-editor --lib -- messages::message::test::generate_message_tree cargo test --package graphite-editor --lib -- messages::message::test::generate_message_tree
node generate.js ../../../hierarchical_message_system_tree.txt replacement.html</pre>" -%} npm run generate-editor-structure</pre>" -%}
{{ content | default(value = fallback) | safe }} {{ content | default(value = fallback) | safe }}
{% endmacro hierarchical_message_system_tree %} {% endmacro hierarchical_message_system_tree %}