justblend 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ build/
2
+ dist/
3
+ *.egg-info/
4
+ __pycache__/
5
+ *.pyc
6
+ *.so
7
+ .pytest_cache/
8
+ log/
@@ -0,0 +1,129 @@
1
+ cmake_minimum_required(VERSION 3.18)
2
+ project(justblend
3
+ VERSION 0.1.0
4
+ DESCRIPTION "Closed-form N-dimensional trajectory generator with smooth corner blends"
5
+ LANGUAGES CXX)
6
+
7
+ set(JUSTBLEND_STANDALONE OFF)
8
+ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
9
+ set(JUSTBLEND_STANDALONE ON)
10
+ endif()
11
+
12
+ option(JUSTBLEND_BUILD_TESTS "Build justblend tests" ${JUSTBLEND_STANDALONE})
13
+ option(JUSTBLEND_BUILD_EXAMPLES "Build justblend examples" ${JUSTBLEND_STANDALONE})
14
+ option(JUSTBLEND_BUILD_PYTHON_BINDINGS "Build justblend pybind11 module" ${JUSTBLEND_STANDALONE})
15
+
16
+ set(CMAKE_CXX_STANDARD 17)
17
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
18
+ set(CMAKE_CXX_EXTENSIONS OFF)
19
+
20
+ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
21
+ set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
22
+ endif()
23
+
24
+ find_package(Eigen3 3.3 REQUIRED NO_MODULE)
25
+
26
+ add_library(justblend STATIC
27
+ src/trajectory.cpp
28
+ src/trajectory_generator.cpp
29
+ src/trapezoidal.cpp
30
+ src/s_curve.cpp
31
+ src/internal/trap_profile.cpp
32
+ src/internal/scurve_profile.cpp
33
+ src/internal/blend.cpp
34
+ src/internal/planner.cpp
35
+ )
36
+ add_library(justblend::justblend ALIAS justblend)
37
+
38
+ target_include_directories(justblend
39
+ PUBLIC
40
+ $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
41
+ $<INSTALL_INTERFACE:include>
42
+ PRIVATE
43
+ ${CMAKE_CURRENT_SOURCE_DIR}/src
44
+ )
45
+ target_link_libraries(justblend PUBLIC Eigen3::Eigen)
46
+ target_compile_features(justblend PUBLIC cxx_std_17)
47
+ set_target_properties(justblend PROPERTIES POSITION_INDEPENDENT_CODE ON)
48
+
49
+ if(MSVC)
50
+ target_compile_options(justblend PRIVATE /W4)
51
+ else()
52
+ target_compile_options(justblend PRIVATE -Wall -Wextra -Wpedantic)
53
+ endif()
54
+
55
+ if(JUSTBLEND_BUILD_PYTHON_BINDINGS)
56
+ find_package(Python COMPONENTS Interpreter Development.Module REQUIRED)
57
+ find_package(pybind11 CONFIG REQUIRED)
58
+ pybind11_add_module(_core MODULE bindings/python.cpp)
59
+ target_link_libraries(_core PRIVATE justblend)
60
+ # Drop the module into the in-tree package so tests and examples can
61
+ # import justblend straight from a source checkout without installing.
62
+ set_target_properties(_core PROPERTIES
63
+ LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/python/justblend
64
+ )
65
+ if(SKBUILD)
66
+ # Wheel builds (scikit-build-core) assemble the Python package from
67
+ # these rules; a plain `cmake --install` must not dump Python files
68
+ # into a C++ prefix, hence the guard.
69
+ install(TARGETS _core DESTINATION justblend)
70
+ install(FILES
71
+ python/justblend/__init__.py
72
+ python/justblend/_core.pyi
73
+ python/justblend/py.typed
74
+ DESTINATION justblend)
75
+ endif()
76
+ endif()
77
+
78
+ if(JUSTBLEND_BUILD_TESTS)
79
+ enable_testing()
80
+ add_subdirectory(tests/cpp)
81
+ endif()
82
+
83
+ if(JUSTBLEND_BUILD_EXAMPLES)
84
+ add_subdirectory(examples/cpp)
85
+ endif()
86
+
87
+ # C++ install rules stay out of wheel builds: a wheel must not ship headers
88
+ # and static libs into the Python platlib root.
89
+ if(NOT SKBUILD)
90
+ install(TARGETS justblend
91
+ EXPORT justblendTargets
92
+ LIBRARY DESTINATION lib
93
+ ARCHIVE DESTINATION lib
94
+ INCLUDES DESTINATION include
95
+ )
96
+ install(DIRECTORY include/justblend DESTINATION include)
97
+
98
+ # Emit justblendConfig.cmake and justblendTargets.cmake so downstream
99
+ # packages can `find_package(justblend REQUIRED)`.
100
+ include(CMakePackageConfigHelpers)
101
+ install(EXPORT justblendTargets
102
+ FILE justblendTargets.cmake
103
+ NAMESPACE justblend::
104
+ DESTINATION lib/cmake/justblend
105
+ )
106
+ configure_package_config_file(
107
+ "${CMAKE_CURRENT_SOURCE_DIR}/cmake/justblendConfig.cmake.in"
108
+ "${CMAKE_CURRENT_BINARY_DIR}/justblendConfig.cmake"
109
+ INSTALL_DESTINATION lib/cmake/justblend
110
+ )
111
+ write_basic_package_version_file(
112
+ "${CMAKE_CURRENT_BINARY_DIR}/justblendConfigVersion.cmake"
113
+ VERSION ${PROJECT_VERSION}
114
+ COMPATIBILITY SameMajorVersion
115
+ )
116
+ install(FILES
117
+ "${CMAKE_CURRENT_BINARY_DIR}/justblendConfig.cmake"
118
+ "${CMAKE_CURRENT_BINARY_DIR}/justblendConfigVersion.cmake"
119
+ DESTINATION lib/cmake/justblend
120
+ )
121
+
122
+ # Also emit justblendTargets.cmake into the build tree so downstream
123
+ # packages can pick up justblend without a separate install step. Consumers
124
+ # set justblend_DIR=<this-build> or add it to CMAKE_PREFIX_PATH.
125
+ export(EXPORT justblendTargets
126
+ FILE "${CMAKE_CURRENT_BINARY_DIR}/justblendTargets.cmake"
127
+ NAMESPACE justblend::
128
+ )
129
+ endif()
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Saif Sidhik
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,248 @@
1
+ Metadata-Version: 2.1
2
+ Name: justblend
3
+ Version: 0.1.0
4
+ Summary: Closed-form N-dimensional trajectory generator with velocity/acceleration/jerk limiting and smooth corner blends
5
+ Author-Email: Saif Sidhik <mail@saifsidhik.page>
6
+ License: MIT
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Programming Language :: C++
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Topic :: Scientific/Engineering
12
+ Project-URL: Repository, https://github.com/justagist/justblend
13
+ Requires-Python: >=3.9
14
+ Requires-Dist: numpy>=1.21
15
+ Provides-Extra: test
16
+ Requires-Dist: pytest>=7; extra == "test"
17
+ Provides-Extra: plot
18
+ Requires-Dist: matplotlib>=3.4; extra == "plot"
19
+ Provides-Extra: bench
20
+ Requires-Dist: ruckig>=0.9; extra == "bench"
21
+ Requires-Dist: toppra>=0.6; extra == "bench"
22
+ Description-Content-Type: text/markdown
23
+
24
+ # justblend
25
+
26
+ Closed-form N-dimensional trajectory generation with smooth corner blends.
27
+ General-purpose: joint trajectories, Cartesian paths, CNC tool paths,
28
+ animation curves -- anywhere you have a list of points in some
29
+ D-dimensional space and want a velocity/acceleration/jerk-limited smooth
30
+ trajectory through them.
31
+
32
+ ![Corner blending and speed profile](docs/blending.png)
33
+
34
+ ## Highlights
35
+
36
+ - **Closed form, microsecond generation.** No optimisation loop, no
37
+ discretisation: typical multi-waypoint problems generate in roughly
38
+ 0.05-0.1 ms and match ruckig's per-segment time-optimal durations on
39
+ polyline motions (see [benchmarks](benchmarks/README.md)).
40
+ - **Two velocity profiles.** Bang-bang **trapezoidal** for the fastest
41
+ rest-to-rest motion, or 7-phase jerk-limited **S-curve** when jerk
42
+ limits are given.
43
+ - **Three corner modes, two blend shapes.** Stop at every waypoint,
44
+ blend through corners, or a hybrid that blends where feasible. The
45
+ cubic-Hermite blend has zero acceleration at its boundaries, so an
46
+ S-curve trajectory stays jerk-limited through the corner.
47
+ - **Analytic guarantees.** Limits are satisfied by construction; the
48
+ corner-cut deviation and per-waypoint passage times come back in
49
+ closed form.
50
+ - **Boundary speeds and duration stretching** for moving-start
51
+ replanning and speed control.
52
+ - **C++17 and Python.** Static library with clean CMake exports;
53
+ pybind11 bindings with type stubs.
54
+
55
+ The output `Trajectory` is lazy: it owns a list of analytic segments and
56
+ can be sampled at any time `t` in `[0, duration]` without precomputing a
57
+ dense buffer.
58
+
59
+ ![Trapezoidal vs S-curve profiles](docs/profiles.png)
60
+
61
+ *(figures regenerate with `python docs/generate_readme_figures.py`)*
62
+
63
+ ## Getting started -- C++
64
+
65
+ ```cpp
66
+ #include <justblend/justblend.hpp>
67
+
68
+ Eigen::MatrixXd waypoints(4, 2);
69
+ waypoints << 0.0, 0.0,
70
+ 1.0, 0.5,
71
+ 1.5,-0.2,
72
+ 0.0, 0.0;
73
+
74
+ justblend::Limits limits;
75
+ limits.v_max = (Eigen::VectorXd(2) << 1.5, 1.2).finished();
76
+ limits.a_max = (Eigen::VectorXd(2) << 3.0, 2.5).finished();
77
+ limits.j_max = (Eigen::VectorXd(2) << 20.0, 18.0).finished();
78
+
79
+ justblend::GenerationOptions opts;
80
+ opts.blend_radius = 0.15;
81
+ opts.corner_handling = justblend::CornerHandling::HYBRID;
82
+
83
+ justblend::SCurveTrajectoryGenerator gen(2);
84
+ gen.setLimits(limits);
85
+ gen.setOptions(opts);
86
+
87
+ auto traj = gen.generate(waypoints);
88
+ auto s = traj.sample(0.5); // single sample
89
+ auto dense = traj.samples(0.002); // uniform dt sweep
90
+ ```
91
+
92
+ ## Getting started -- Python
93
+
94
+ ```python
95
+ import numpy as np
96
+ import justblend as jb
97
+
98
+ waypoints = np.array([[0.0, 0.0], [1.0, 0.5], [1.5, -0.2], [0.0, 0.0]])
99
+ limits = jb.Limits(
100
+ v_max=np.array([1.5, 1.2]),
101
+ a_max=np.array([3.0, 2.5]),
102
+ j_max=np.array([20.0, 18.0]),
103
+ )
104
+ opts = jb.GenerationOptions(
105
+ blend_radius=0.15,
106
+ corner_handling=jb.CornerHandling.HYBRID,
107
+ )
108
+
109
+ gen = jb.SCurveTrajectoryGenerator(dim=2)
110
+ gen.set_limits(limits)
111
+ gen.set_options(opts)
112
+ traj = gen.generate(waypoints)
113
+
114
+ s = traj.sample(0.5)
115
+ dense = traj.samples(dt=0.002)
116
+ arbitrary = traj.samples(np.linspace(0, traj.duration(), 1000))
117
+ ```
118
+
119
+ ## Corner semantics
120
+
121
+ - `blend_radius` is the cut-back distance along each adjacent leg of the
122
+ corner, not an arc radius: a blend replaces the final `r` of the incoming
123
+ segment and the first `r` of the outgoing one.
124
+ - Blended corners cut the corner -- the trajectory does not pass through a
125
+ blended interior waypoint. Use `StrictCorners` (or a per-corner radius via
126
+ `blend_radii`) where exact pass-through matters.
127
+ - Corners within ~2.6 degrees of a full reversal (the path doubles back on
128
+ itself) cannot be blended: `Hybrid` falls back to a full stop at the
129
+ waypoint, `UseBlending` raises a `ValidationError`.
130
+ - An S-curve trajectory is jerk-limited end to end only with the default
131
+ Hermite blends. Forcing `blend_shape = Parabolic` on the S-curve generator
132
+ keeps the velocity and acceleration limits but introduces acceleration
133
+ steps at blend boundaries, i.e. unbounded jerk there.
134
+
135
+ All input-validation failures throw `justblend::ValidationError`, which the
136
+ Python bindings raise as a `ValueError` subclass.
137
+
138
+ ## Boundary speeds, timing and deviation queries
139
+
140
+ `generate()` accepts optional tangential boundary speeds along the first and
141
+ last segment direction; infeasible requests (too fast for the limits or for
142
+ the available distance) raise a `ValidationError`:
143
+
144
+ ```cpp
145
+ auto traj = gen.generate(waypoints, /*v_start=*/0.3, /*v_end=*/0.0);
146
+ ```
147
+
148
+ ```python
149
+ traj = gen.generate(waypoints, v_start=0.3, v_end=0.0)
150
+ ```
151
+
152
+ `GenerationOptions` carries `velocity_scaling_factor` and
153
+ `acceleration_scaling_factor` in `(0, 1]` (matching MoveIt's semantics):
154
+ they scale `v_max` / `a_max` at generation time; `j_max` is never scaled.
155
+
156
+ The `Trajectory` exposes timing and corner-cut metadata:
157
+
158
+ - `segment_start_times()` -- start time of each segment plus the total
159
+ duration as the last entry.
160
+ - `waypoint_times()` -- passage time per waypoint; for blended corners this
161
+ is the time of closest approach (the blend midpoint).
162
+ - `corner_deviations()` / `max_corner_deviation()` -- closed-form distance
163
+ between each blended corner's waypoint and the blend path
164
+ (`(r/4)·|d_out - d_in|` for parabolic, `(3r/16)·|d_out - d_in|` for
165
+ Hermite blends; zero for non-blend corners). Useful to bound how far the
166
+ blended trajectory strays from the original polyline.
167
+ - `stretched_to(duration)` -- uniformly time-dilated copy with exactly the
168
+ requested total duration (same path; velocities scale by `T/duration`,
169
+ accelerations quadratically, jerks cubically, so all limits remain
170
+ satisfied). Durations shorter than the current one raise a
171
+ `ValidationError`.
172
+
173
+ ## Install -- C++
174
+
175
+ Requirements: CMake ≥ 3.18, a C++17 compiler, and Eigen 3.3+ (any package
176
+ that exposes `find_package(Eigen3 NO_MODULE)` -- the Debian/Ubuntu
177
+ `libeigen3-dev` package, vcpkg, conan, brew, or a manual install all work).
178
+
179
+ ### Build and install to a prefix
180
+
181
+ ```bash
182
+ cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \
183
+ -DJUSTBLEND_BUILD_TESTS=OFF \
184
+ -DJUSTBLEND_BUILD_EXAMPLES=OFF \
185
+ -DJUSTBLEND_BUILD_PYTHON_BINDINGS=OFF
186
+ cmake --build build --parallel
187
+ cmake --install build --prefix /your/install/prefix # omit --prefix for /usr/local
188
+ ```
189
+
190
+ This installs:
191
+
192
+ - `<prefix>/include/justblend/*.hpp`
193
+ - `<prefix>/lib/libjustblend.a`
194
+ - `<prefix>/lib/cmake/justblend/justblend{Config,ConfigVersion,Targets}.cmake`
195
+
196
+ ### Use from another CMake project
197
+
198
+ After installing, consume justblend with `find_package`:
199
+
200
+ ```cmake
201
+ find_package(justblend 0.1.0 REQUIRED)
202
+
203
+ add_executable(my_app main.cpp)
204
+ target_link_libraries(my_app PRIVATE justblend::justblend)
205
+ ```
206
+
207
+ Then configure your project with `-DCMAKE_PREFIX_PATH=/your/install/prefix`
208
+ (or set `justblend_DIR=<prefix>/lib/cmake/justblend`).
209
+
210
+ You don't have to install: justblend also exports its targets from the
211
+ build tree, so pointing `CMAKE_PREFIX_PATH` at the build directory works
212
+ just as well.
213
+
214
+ To vendor justblend as a subdirectory (git submodule, FetchContent, or a
215
+ plain copy), drop `add_subdirectory(path/to/justblend)` into your
216
+ `CMakeLists.txt` -- the same `justblend::justblend` target becomes
217
+ available, and the test / example / Python-binding targets stay OFF by
218
+ default when consumed this way.
219
+
220
+ ### Run tests and examples
221
+
222
+ ```bash
223
+ cmake -S . -B build -DJUSTBLEND_BUILD_TESTS=ON -DJUSTBLEND_BUILD_EXAMPLES=ON
224
+ cmake --build build --parallel
225
+ ctest --test-dir build --output-on-failure
226
+ ./build/examples/cpp/example_basic_2d
227
+ ```
228
+
229
+ ## Install -- Python
230
+
231
+ ```bash
232
+ pip install .
233
+ ```
234
+
235
+ `pip install` fetches `scikit-build-core` and `pybind11` automatically and
236
+ drives the same CMake build under the hood -- no separate C++ install step
237
+ needed. Eigen 3.3+ must still be installed on the host.
238
+
239
+ ## Benchmarks
240
+
241
+ justblend is benchmarked against ruckig and toppra on shared scenarios;
242
+ instructions, result tables and plots live in
243
+ [benchmarks/README.md](benchmarks/README.md) (regenerated by
244
+ `python benchmarks/run.py`).
245
+
246
+ ## License
247
+
248
+ MIT -- see [LICENSE](LICENSE).
@@ -0,0 +1,225 @@
1
+ # justblend
2
+
3
+ Closed-form N-dimensional trajectory generation with smooth corner blends.
4
+ General-purpose: joint trajectories, Cartesian paths, CNC tool paths,
5
+ animation curves -- anywhere you have a list of points in some
6
+ D-dimensional space and want a velocity/acceleration/jerk-limited smooth
7
+ trajectory through them.
8
+
9
+ ![Corner blending and speed profile](docs/blending.png)
10
+
11
+ ## Highlights
12
+
13
+ - **Closed form, microsecond generation.** No optimisation loop, no
14
+ discretisation: typical multi-waypoint problems generate in roughly
15
+ 0.05-0.1 ms and match ruckig's per-segment time-optimal durations on
16
+ polyline motions (see [benchmarks](benchmarks/README.md)).
17
+ - **Two velocity profiles.** Bang-bang **trapezoidal** for the fastest
18
+ rest-to-rest motion, or 7-phase jerk-limited **S-curve** when jerk
19
+ limits are given.
20
+ - **Three corner modes, two blend shapes.** Stop at every waypoint,
21
+ blend through corners, or a hybrid that blends where feasible. The
22
+ cubic-Hermite blend has zero acceleration at its boundaries, so an
23
+ S-curve trajectory stays jerk-limited through the corner.
24
+ - **Analytic guarantees.** Limits are satisfied by construction; the
25
+ corner-cut deviation and per-waypoint passage times come back in
26
+ closed form.
27
+ - **Boundary speeds and duration stretching** for moving-start
28
+ replanning and speed control.
29
+ - **C++17 and Python.** Static library with clean CMake exports;
30
+ pybind11 bindings with type stubs.
31
+
32
+ The output `Trajectory` is lazy: it owns a list of analytic segments and
33
+ can be sampled at any time `t` in `[0, duration]` without precomputing a
34
+ dense buffer.
35
+
36
+ ![Trapezoidal vs S-curve profiles](docs/profiles.png)
37
+
38
+ *(figures regenerate with `python docs/generate_readme_figures.py`)*
39
+
40
+ ## Getting started -- C++
41
+
42
+ ```cpp
43
+ #include <justblend/justblend.hpp>
44
+
45
+ Eigen::MatrixXd waypoints(4, 2);
46
+ waypoints << 0.0, 0.0,
47
+ 1.0, 0.5,
48
+ 1.5,-0.2,
49
+ 0.0, 0.0;
50
+
51
+ justblend::Limits limits;
52
+ limits.v_max = (Eigen::VectorXd(2) << 1.5, 1.2).finished();
53
+ limits.a_max = (Eigen::VectorXd(2) << 3.0, 2.5).finished();
54
+ limits.j_max = (Eigen::VectorXd(2) << 20.0, 18.0).finished();
55
+
56
+ justblend::GenerationOptions opts;
57
+ opts.blend_radius = 0.15;
58
+ opts.corner_handling = justblend::CornerHandling::HYBRID;
59
+
60
+ justblend::SCurveTrajectoryGenerator gen(2);
61
+ gen.setLimits(limits);
62
+ gen.setOptions(opts);
63
+
64
+ auto traj = gen.generate(waypoints);
65
+ auto s = traj.sample(0.5); // single sample
66
+ auto dense = traj.samples(0.002); // uniform dt sweep
67
+ ```
68
+
69
+ ## Getting started -- Python
70
+
71
+ ```python
72
+ import numpy as np
73
+ import justblend as jb
74
+
75
+ waypoints = np.array([[0.0, 0.0], [1.0, 0.5], [1.5, -0.2], [0.0, 0.0]])
76
+ limits = jb.Limits(
77
+ v_max=np.array([1.5, 1.2]),
78
+ a_max=np.array([3.0, 2.5]),
79
+ j_max=np.array([20.0, 18.0]),
80
+ )
81
+ opts = jb.GenerationOptions(
82
+ blend_radius=0.15,
83
+ corner_handling=jb.CornerHandling.HYBRID,
84
+ )
85
+
86
+ gen = jb.SCurveTrajectoryGenerator(dim=2)
87
+ gen.set_limits(limits)
88
+ gen.set_options(opts)
89
+ traj = gen.generate(waypoints)
90
+
91
+ s = traj.sample(0.5)
92
+ dense = traj.samples(dt=0.002)
93
+ arbitrary = traj.samples(np.linspace(0, traj.duration(), 1000))
94
+ ```
95
+
96
+ ## Corner semantics
97
+
98
+ - `blend_radius` is the cut-back distance along each adjacent leg of the
99
+ corner, not an arc radius: a blend replaces the final `r` of the incoming
100
+ segment and the first `r` of the outgoing one.
101
+ - Blended corners cut the corner -- the trajectory does not pass through a
102
+ blended interior waypoint. Use `StrictCorners` (or a per-corner radius via
103
+ `blend_radii`) where exact pass-through matters.
104
+ - Corners within ~2.6 degrees of a full reversal (the path doubles back on
105
+ itself) cannot be blended: `Hybrid` falls back to a full stop at the
106
+ waypoint, `UseBlending` raises a `ValidationError`.
107
+ - An S-curve trajectory is jerk-limited end to end only with the default
108
+ Hermite blends. Forcing `blend_shape = Parabolic` on the S-curve generator
109
+ keeps the velocity and acceleration limits but introduces acceleration
110
+ steps at blend boundaries, i.e. unbounded jerk there.
111
+
112
+ All input-validation failures throw `justblend::ValidationError`, which the
113
+ Python bindings raise as a `ValueError` subclass.
114
+
115
+ ## Boundary speeds, timing and deviation queries
116
+
117
+ `generate()` accepts optional tangential boundary speeds along the first and
118
+ last segment direction; infeasible requests (too fast for the limits or for
119
+ the available distance) raise a `ValidationError`:
120
+
121
+ ```cpp
122
+ auto traj = gen.generate(waypoints, /*v_start=*/0.3, /*v_end=*/0.0);
123
+ ```
124
+
125
+ ```python
126
+ traj = gen.generate(waypoints, v_start=0.3, v_end=0.0)
127
+ ```
128
+
129
+ `GenerationOptions` carries `velocity_scaling_factor` and
130
+ `acceleration_scaling_factor` in `(0, 1]` (matching MoveIt's semantics):
131
+ they scale `v_max` / `a_max` at generation time; `j_max` is never scaled.
132
+
133
+ The `Trajectory` exposes timing and corner-cut metadata:
134
+
135
+ - `segment_start_times()` -- start time of each segment plus the total
136
+ duration as the last entry.
137
+ - `waypoint_times()` -- passage time per waypoint; for blended corners this
138
+ is the time of closest approach (the blend midpoint).
139
+ - `corner_deviations()` / `max_corner_deviation()` -- closed-form distance
140
+ between each blended corner's waypoint and the blend path
141
+ (`(r/4)·|d_out - d_in|` for parabolic, `(3r/16)·|d_out - d_in|` for
142
+ Hermite blends; zero for non-blend corners). Useful to bound how far the
143
+ blended trajectory strays from the original polyline.
144
+ - `stretched_to(duration)` -- uniformly time-dilated copy with exactly the
145
+ requested total duration (same path; velocities scale by `T/duration`,
146
+ accelerations quadratically, jerks cubically, so all limits remain
147
+ satisfied). Durations shorter than the current one raise a
148
+ `ValidationError`.
149
+
150
+ ## Install -- C++
151
+
152
+ Requirements: CMake ≥ 3.18, a C++17 compiler, and Eigen 3.3+ (any package
153
+ that exposes `find_package(Eigen3 NO_MODULE)` -- the Debian/Ubuntu
154
+ `libeigen3-dev` package, vcpkg, conan, brew, or a manual install all work).
155
+
156
+ ### Build and install to a prefix
157
+
158
+ ```bash
159
+ cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \
160
+ -DJUSTBLEND_BUILD_TESTS=OFF \
161
+ -DJUSTBLEND_BUILD_EXAMPLES=OFF \
162
+ -DJUSTBLEND_BUILD_PYTHON_BINDINGS=OFF
163
+ cmake --build build --parallel
164
+ cmake --install build --prefix /your/install/prefix # omit --prefix for /usr/local
165
+ ```
166
+
167
+ This installs:
168
+
169
+ - `<prefix>/include/justblend/*.hpp`
170
+ - `<prefix>/lib/libjustblend.a`
171
+ - `<prefix>/lib/cmake/justblend/justblend{Config,ConfigVersion,Targets}.cmake`
172
+
173
+ ### Use from another CMake project
174
+
175
+ After installing, consume justblend with `find_package`:
176
+
177
+ ```cmake
178
+ find_package(justblend 0.1.0 REQUIRED)
179
+
180
+ add_executable(my_app main.cpp)
181
+ target_link_libraries(my_app PRIVATE justblend::justblend)
182
+ ```
183
+
184
+ Then configure your project with `-DCMAKE_PREFIX_PATH=/your/install/prefix`
185
+ (or set `justblend_DIR=<prefix>/lib/cmake/justblend`).
186
+
187
+ You don't have to install: justblend also exports its targets from the
188
+ build tree, so pointing `CMAKE_PREFIX_PATH` at the build directory works
189
+ just as well.
190
+
191
+ To vendor justblend as a subdirectory (git submodule, FetchContent, or a
192
+ plain copy), drop `add_subdirectory(path/to/justblend)` into your
193
+ `CMakeLists.txt` -- the same `justblend::justblend` target becomes
194
+ available, and the test / example / Python-binding targets stay OFF by
195
+ default when consumed this way.
196
+
197
+ ### Run tests and examples
198
+
199
+ ```bash
200
+ cmake -S . -B build -DJUSTBLEND_BUILD_TESTS=ON -DJUSTBLEND_BUILD_EXAMPLES=ON
201
+ cmake --build build --parallel
202
+ ctest --test-dir build --output-on-failure
203
+ ./build/examples/cpp/example_basic_2d
204
+ ```
205
+
206
+ ## Install -- Python
207
+
208
+ ```bash
209
+ pip install .
210
+ ```
211
+
212
+ `pip install` fetches `scikit-build-core` and `pybind11` automatically and
213
+ drives the same CMake build under the hood -- no separate C++ install step
214
+ needed. Eigen 3.3+ must still be installed on the host.
215
+
216
+ ## Benchmarks
217
+
218
+ justblend is benchmarked against ruckig and toppra on shared scenarios;
219
+ instructions, result tables and plots live in
220
+ [benchmarks/README.md](benchmarks/README.md) (regenerated by
221
+ `python benchmarks/run.py`).
222
+
223
+ ## License
224
+
225
+ MIT -- see [LICENSE](LICENSE).