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.
- justblend-0.1.0/.gitignore +8 -0
- justblend-0.1.0/CMakeLists.txt +129 -0
- justblend-0.1.0/LICENSE +21 -0
- justblend-0.1.0/PKG-INFO +248 -0
- justblend-0.1.0/README.md +225 -0
- justblend-0.1.0/bindings/python.cpp +164 -0
- justblend-0.1.0/cmake/justblendConfig.cmake.in +8 -0
- justblend-0.1.0/include/justblend/justblend.hpp +10 -0
- justblend-0.1.0/include/justblend/s_curve.hpp +24 -0
- justblend-0.1.0/include/justblend/trajectory.hpp +91 -0
- justblend-0.1.0/include/justblend/trajectory_generator.hpp +57 -0
- justblend-0.1.0/include/justblend/trapezoidal.hpp +25 -0
- justblend-0.1.0/include/justblend/types.hpp +115 -0
- justblend-0.1.0/pyproject.toml +58 -0
- justblend-0.1.0/python/justblend/__init__.py +37 -0
- justblend-0.1.0/python/justblend/_core.pyi +121 -0
- justblend-0.1.0/python/justblend/py.typed +0 -0
- justblend-0.1.0/src/internal/blend.cpp +77 -0
- justblend-0.1.0/src/internal/blend.hpp +29 -0
- justblend-0.1.0/src/internal/planner.cpp +450 -0
- justblend-0.1.0/src/internal/planner.hpp +20 -0
- justblend-0.1.0/src/internal/scurve_profile.cpp +138 -0
- justblend-0.1.0/src/internal/scurve_profile.hpp +30 -0
- justblend-0.1.0/src/internal/segment.hpp +77 -0
- justblend-0.1.0/src/internal/trap_profile.cpp +63 -0
- justblend-0.1.0/src/internal/trap_profile.hpp +26 -0
- justblend-0.1.0/src/s_curve.cpp +14 -0
- justblend-0.1.0/src/trajectory.cpp +335 -0
- justblend-0.1.0/src/trajectory_generator.cpp +101 -0
- justblend-0.1.0/src/trapezoidal.cpp +11 -0
|
@@ -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()
|
justblend-0.1.0/LICENSE
ADDED
|
@@ -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.
|
justblend-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

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