cartesian-tree 0.1.0__tar.gz → 0.2.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.
- {cartesian_tree-0.1.0 → cartesian_tree-0.2.0}/.github/workflows/ci.yaml +23 -4
- cartesian_tree-0.2.0/.github/workflows/publish.yaml +61 -0
- {cartesian_tree-0.1.0 → cartesian_tree-0.2.0}/.gitignore +4 -1
- {cartesian_tree-0.1.0 → cartesian_tree-0.2.0}/Cargo.lock +66 -1
- {cartesian_tree-0.1.0 → cartesian_tree-0.2.0}/Cargo.toml +6 -4
- {cartesian_tree-0.1.0 → cartesian_tree-0.2.0}/PKG-INFO +1 -1
- {cartesian_tree-0.1.0 → cartesian_tree-0.2.0}/pyproject.toml +1 -0
- {cartesian_tree-0.1.0 → cartesian_tree-0.2.0}/python/cartesian_tree/helper.py +1 -1
- {cartesian_tree-0.1.0 → cartesian_tree-0.2.0}/python/cartesian_tree/lib.py +67 -5
- {cartesian_tree-0.1.0 → cartesian_tree-0.2.0}/python/tests/test_lib.py +52 -0
- {cartesian_tree-0.1.0 → cartesian_tree-0.2.0}/src/bindings/frame.rs +36 -1
- {cartesian_tree-0.1.0 → cartesian_tree-0.2.0}/src/bindings/pose.rs +7 -5
- {cartesian_tree-0.1.0 → cartesian_tree-0.2.0}/src/errors.rs +4 -0
- {cartesian_tree-0.1.0 → cartesian_tree-0.2.0}/src/frame.rs +287 -3
- {cartesian_tree-0.1.0 → cartesian_tree-0.2.0}/src/lib.rs +1 -1
- {cartesian_tree-0.1.0 → cartesian_tree-0.2.0}/src/pose.rs +5 -9
- {cartesian_tree-0.1.0 → cartesian_tree-0.2.0}/src/tree/traits.rs +24 -0
- cartesian_tree-0.1.0/.github/workflows/publish.yaml +0 -40
- {cartesian_tree-0.1.0 → cartesian_tree-0.2.0}/.github/CODEOWNERS +0 -0
- {cartesian_tree-0.1.0 → cartesian_tree-0.2.0}/.github/workflows/release.yaml +0 -0
- {cartesian_tree-0.1.0 → cartesian_tree-0.2.0}/README.md +0 -0
- {cartesian_tree-0.1.0 → cartesian_tree-0.2.0}/license-apache +0 -0
- {cartesian_tree-0.1.0 → cartesian_tree-0.2.0}/license-mit +0 -0
- {cartesian_tree-0.1.0 → cartesian_tree-0.2.0}/python/cartesian_tree/__init__.py +0 -0
- {cartesian_tree-0.1.0 → cartesian_tree-0.2.0}/python/cartesian_tree/py.typed +0 -0
- {cartesian_tree-0.1.0 → cartesian_tree-0.2.0}/python/tests/__init__.py +0 -0
- {cartesian_tree-0.1.0 → cartesian_tree-0.2.0}/src/bindings/mod.rs +0 -0
- {cartesian_tree-0.1.0 → cartesian_tree-0.2.0}/src/bindings/utils.rs +0 -0
- {cartesian_tree-0.1.0 → cartesian_tree-0.2.0}/src/orientation.rs +0 -0
- {cartesian_tree-0.1.0 → cartesian_tree-0.2.0}/src/tree/mod.rs +0 -0
|
@@ -15,18 +15,36 @@ env:
|
|
|
15
15
|
|
|
16
16
|
jobs:
|
|
17
17
|
test:
|
|
18
|
-
name: Test
|
|
19
|
-
runs-on:
|
|
18
|
+
name: Test (${{ matrix.os }} / py${{ matrix.python-version }})
|
|
19
|
+
runs-on: ${{ matrix.os }}
|
|
20
20
|
strategy:
|
|
21
21
|
fail-fast: false
|
|
22
22
|
matrix:
|
|
23
|
-
|
|
23
|
+
os: [ubuntu-latest, macos-latest]
|
|
24
|
+
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
|
24
25
|
|
|
25
26
|
steps:
|
|
26
27
|
- uses: actions/checkout@v4
|
|
27
28
|
|
|
28
29
|
- name: Install Rust toolchain
|
|
29
30
|
uses: dtolnay/rust-toolchain@stable
|
|
31
|
+
with:
|
|
32
|
+
components: rustfmt, clippy
|
|
33
|
+
|
|
34
|
+
- name: Cache cargo
|
|
35
|
+
uses: actions/cache@v4
|
|
36
|
+
with:
|
|
37
|
+
path: |
|
|
38
|
+
~/.cargo/registry
|
|
39
|
+
~/.cargo/git
|
|
40
|
+
target
|
|
41
|
+
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
42
|
+
|
|
43
|
+
- name: Cache pip
|
|
44
|
+
uses: actions/cache@v4
|
|
45
|
+
with:
|
|
46
|
+
path: ~/.cache/pip
|
|
47
|
+
key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }}
|
|
30
48
|
|
|
31
49
|
- name: Run Rust fmt
|
|
32
50
|
run: cargo fmt --check
|
|
@@ -69,4 +87,5 @@ jobs:
|
|
|
69
87
|
- name: Run Python tests
|
|
70
88
|
run: |
|
|
71
89
|
source .venv/bin/activate
|
|
72
|
-
pytest python/tests
|
|
90
|
+
pytest python/tests
|
|
91
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_call:
|
|
5
|
+
secrets:
|
|
6
|
+
CARGO_REGISTRY_TOKEN:
|
|
7
|
+
required: true
|
|
8
|
+
PYPI_TOKEN:
|
|
9
|
+
required: true
|
|
10
|
+
|
|
11
|
+
env:
|
|
12
|
+
CARGO_TERM_COLOR: always
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
publish-rust:
|
|
16
|
+
name: Publish Rust Crate
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
- name: Install Rust toolchain
|
|
21
|
+
uses: dtolnay/rust-toolchain@stable
|
|
22
|
+
|
|
23
|
+
- name: Publish to crates.io
|
|
24
|
+
run: cargo publish --token ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
|
25
|
+
|
|
26
|
+
publish-python:
|
|
27
|
+
name: Publish Python Package
|
|
28
|
+
runs-on: ${{ matrix.os }}
|
|
29
|
+
strategy:
|
|
30
|
+
fail-fast: false
|
|
31
|
+
matrix:
|
|
32
|
+
os: [ubuntu-latest, macos-latest]
|
|
33
|
+
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
|
34
|
+
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v4
|
|
37
|
+
- name: Setup Python
|
|
38
|
+
uses: actions/setup-python@v5
|
|
39
|
+
with:
|
|
40
|
+
python-version: ${{ matrix.python-version }}
|
|
41
|
+
|
|
42
|
+
- name: Build and publish wheels
|
|
43
|
+
uses: messense/maturin-action@v1
|
|
44
|
+
with:
|
|
45
|
+
maturin-version: latest
|
|
46
|
+
command: publish
|
|
47
|
+
args: --username __token__ --password ${{ secrets.PYPI_TOKEN }} --skip-existing
|
|
48
|
+
manylinux: auto
|
|
49
|
+
|
|
50
|
+
publish-sdist:
|
|
51
|
+
name: Publish sdist (source)
|
|
52
|
+
runs-on: ubuntu-latest
|
|
53
|
+
steps:
|
|
54
|
+
- uses: actions/checkout@v4
|
|
55
|
+
|
|
56
|
+
- uses: actions/setup-python@v5
|
|
57
|
+
with:
|
|
58
|
+
python-version: "3.12"
|
|
59
|
+
|
|
60
|
+
- name: Build and publish sdist
|
|
61
|
+
run: maturin publish --sdist --username __token__ --password ${{ secrets.PYPI_TOKEN }} --skip-existing
|
|
@@ -4,10 +4,12 @@ version = 4
|
|
|
4
4
|
|
|
5
5
|
[[package]]
|
|
6
6
|
name = "CartesianTree"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
dependencies = [
|
|
9
9
|
"nalgebra",
|
|
10
10
|
"pyo3",
|
|
11
|
+
"serde",
|
|
12
|
+
"serde_json",
|
|
11
13
|
"thiserror",
|
|
12
14
|
]
|
|
13
15
|
|
|
@@ -44,6 +46,12 @@ version = "2.0.6"
|
|
|
44
46
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
45
47
|
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
|
|
46
48
|
|
|
49
|
+
[[package]]
|
|
50
|
+
name = "itoa"
|
|
51
|
+
version = "1.0.15"
|
|
52
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
53
|
+
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
|
54
|
+
|
|
47
55
|
[[package]]
|
|
48
56
|
name = "libc"
|
|
49
57
|
version = "0.2.172"
|
|
@@ -60,6 +68,12 @@ dependencies = [
|
|
|
60
68
|
"rawpointer",
|
|
61
69
|
]
|
|
62
70
|
|
|
71
|
+
[[package]]
|
|
72
|
+
name = "memchr"
|
|
73
|
+
version = "2.7.6"
|
|
74
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
75
|
+
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
|
76
|
+
|
|
63
77
|
[[package]]
|
|
64
78
|
name = "memoffset"
|
|
65
79
|
version = "0.9.1"
|
|
@@ -81,6 +95,7 @@ dependencies = [
|
|
|
81
95
|
"num-complex",
|
|
82
96
|
"num-rational",
|
|
83
97
|
"num-traits",
|
|
98
|
+
"serde",
|
|
84
99
|
"simba",
|
|
85
100
|
"typenum",
|
|
86
101
|
]
|
|
@@ -113,6 +128,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
|
113
128
|
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
|
|
114
129
|
dependencies = [
|
|
115
130
|
"num-traits",
|
|
131
|
+
"serde",
|
|
116
132
|
]
|
|
117
133
|
|
|
118
134
|
[[package]]
|
|
@@ -248,6 +264,12 @@ version = "0.2.1"
|
|
|
248
264
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
249
265
|
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
|
|
250
266
|
|
|
267
|
+
[[package]]
|
|
268
|
+
name = "ryu"
|
|
269
|
+
version = "1.0.20"
|
|
270
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
271
|
+
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
|
272
|
+
|
|
251
273
|
[[package]]
|
|
252
274
|
name = "safe_arch"
|
|
253
275
|
version = "0.7.4"
|
|
@@ -257,6 +279,49 @@ dependencies = [
|
|
|
257
279
|
"bytemuck",
|
|
258
280
|
]
|
|
259
281
|
|
|
282
|
+
[[package]]
|
|
283
|
+
name = "serde"
|
|
284
|
+
version = "1.0.225"
|
|
285
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
286
|
+
checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d"
|
|
287
|
+
dependencies = [
|
|
288
|
+
"serde_core",
|
|
289
|
+
"serde_derive",
|
|
290
|
+
]
|
|
291
|
+
|
|
292
|
+
[[package]]
|
|
293
|
+
name = "serde_core"
|
|
294
|
+
version = "1.0.225"
|
|
295
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
296
|
+
checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383"
|
|
297
|
+
dependencies = [
|
|
298
|
+
"serde_derive",
|
|
299
|
+
]
|
|
300
|
+
|
|
301
|
+
[[package]]
|
|
302
|
+
name = "serde_derive"
|
|
303
|
+
version = "1.0.225"
|
|
304
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
305
|
+
checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516"
|
|
306
|
+
dependencies = [
|
|
307
|
+
"proc-macro2",
|
|
308
|
+
"quote",
|
|
309
|
+
"syn",
|
|
310
|
+
]
|
|
311
|
+
|
|
312
|
+
[[package]]
|
|
313
|
+
name = "serde_json"
|
|
314
|
+
version = "1.0.145"
|
|
315
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
316
|
+
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
|
|
317
|
+
dependencies = [
|
|
318
|
+
"itoa",
|
|
319
|
+
"memchr",
|
|
320
|
+
"ryu",
|
|
321
|
+
"serde",
|
|
322
|
+
"serde_core",
|
|
323
|
+
]
|
|
324
|
+
|
|
260
325
|
[[package]]
|
|
261
326
|
name = "simba"
|
|
262
327
|
version = "0.9.0"
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
authors = ["Kn0g"]
|
|
3
3
|
name = "CartesianTree"
|
|
4
|
-
version = "0.
|
|
4
|
+
version = "0.2.0"
|
|
5
5
|
edition = "2024"
|
|
6
6
|
description = "Build hierarchical Cartesian coordinate systems to easily transform poses."
|
|
7
7
|
homepage = "https://github.com/Kn0g/cartesian-tree"
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
repository = "https://github.com/Kn0g/cartesian-tree"
|
|
10
10
|
license = "MIT"
|
|
11
|
-
keywords = ["Cartesian", "coordinate
|
|
12
|
-
categories = ["
|
|
11
|
+
keywords = ["Cartesian", "coordinate-systems", "transform", "poses"]
|
|
12
|
+
categories = ["mathematics", "data-structures"]
|
|
13
13
|
|
|
14
14
|
[dependencies]
|
|
15
|
-
nalgebra = "0.33.2"
|
|
15
|
+
nalgebra = { version = "0.33.2", features = ["serde-serialize"] }
|
|
16
16
|
pyo3 = { version = "0.25.0", features = ["extension-module"] , optional = true}
|
|
17
17
|
thiserror = "2.0.12"
|
|
18
|
+
serde = { version = "1.0", features = ["derive"] }
|
|
19
|
+
serde_json = "1.0"
|
|
18
20
|
|
|
19
21
|
[features]
|
|
20
22
|
bindings = ["dep:pyo3"]
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
7
|
from .helper import RPY, Position, Quaternion
|
|
8
|
-
from cartesian_tree import
|
|
8
|
+
from cartesian_tree import _cartesian_tree as _core # type: ignore[attr-defined]
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class Frame:
|
|
@@ -53,6 +53,34 @@ class Frame:
|
|
|
53
53
|
binding_frame = self._core_frame.add_child(name, position._binding_structure, orientation._binding_structure)
|
|
54
54
|
return Frame._from_rust(binding_frame)
|
|
55
55
|
|
|
56
|
+
def calibrate_child(
|
|
57
|
+
self, name: str, desired_position: Position, desired_orientation: RPY | Quaternion, reference_pose: Pose
|
|
58
|
+
) -> Frame:
|
|
59
|
+
"""Adds a child frame such that a reference pose, expressed in the new frame, matches the desired isometry.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
name: The name of the new child frame.
|
|
63
|
+
desired_position: The desired position of the reference pose in the new frame.
|
|
64
|
+
desired_orientation: The desired orientation of the reference pose in the new frame.
|
|
65
|
+
reference_pose: The reference pose for calibration.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The newly created child frame.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ValueError: If a child with the same name already exists.
|
|
72
|
+
"""
|
|
73
|
+
if isinstance(desired_orientation, RPY):
|
|
74
|
+
desired_orientation = desired_orientation.to_quaternion()
|
|
75
|
+
|
|
76
|
+
binding_frame = self._core_frame.calibrate_child(
|
|
77
|
+
name,
|
|
78
|
+
desired_position._binding_structure,
|
|
79
|
+
desired_orientation._binding_structure,
|
|
80
|
+
reference_pose._binding_structure,
|
|
81
|
+
)
|
|
82
|
+
return Frame._from_rust(binding_frame)
|
|
83
|
+
|
|
56
84
|
def add_pose(self, position: Position, orientation: RPY | Quaternion) -> Pose:
|
|
57
85
|
"""Adds a pose to the current frame.
|
|
58
86
|
|
|
@@ -98,6 +126,28 @@ class Frame:
|
|
|
98
126
|
orientation = orientation.to_quaternion()
|
|
99
127
|
self._core_frame.update_transformation(position._binding_structure, orientation._binding_structure)
|
|
100
128
|
|
|
129
|
+
def to_json(self) -> str:
|
|
130
|
+
"""Serializes the frame tree to a JSON string.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
The JSON representation of the tree.
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
ValueError: On serialization failure.
|
|
137
|
+
"""
|
|
138
|
+
return self._core_frame.to_json()
|
|
139
|
+
|
|
140
|
+
def apply_config(self, config_json: str) -> None:
|
|
141
|
+
"""Applies a JSON config to update matching transforms in the tree.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
config_json: The JSON string to apply.
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
ValueError: On deserialization or mismatch errors.
|
|
148
|
+
"""
|
|
149
|
+
self._core_frame.apply_config(config_json)
|
|
150
|
+
|
|
101
151
|
def parent(self) -> Frame | None:
|
|
102
152
|
"""Returns the parent of the frame.
|
|
103
153
|
|
|
@@ -109,6 +159,15 @@ class Frame:
|
|
|
109
159
|
return None
|
|
110
160
|
return Frame._from_rust(binding_parent)
|
|
111
161
|
|
|
162
|
+
def root(self) -> Frame:
|
|
163
|
+
"""Returns the root frame of the tree.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
The root frame of the tree.
|
|
167
|
+
"""
|
|
168
|
+
binding_root = self._core_frame.root()
|
|
169
|
+
return Frame._from_rust(binding_root)
|
|
170
|
+
|
|
112
171
|
def children(self) -> list[Frame]:
|
|
113
172
|
"""Returns the children of the frame.
|
|
114
173
|
|
|
@@ -139,10 +198,9 @@ class Pose:
|
|
|
139
198
|
|
|
140
199
|
_core_pose: _core.Pose
|
|
141
200
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return self._core_pose.frame_name
|
|
201
|
+
def frame(self) -> Frame:
|
|
202
|
+
"""Returns the frame of the pose."""
|
|
203
|
+
return Frame._from_rust(self._core_pose.frame())
|
|
146
204
|
|
|
147
205
|
def transformation(self) -> tuple[Position, Quaternion]:
|
|
148
206
|
"""Returns the transformation of the pose to its parent frame.
|
|
@@ -179,6 +237,10 @@ class Pose:
|
|
|
179
237
|
binding_pose = self._core_pose.in_frame(target_frame._binding_structure)
|
|
180
238
|
return Pose._from_rust(binding_pose)
|
|
181
239
|
|
|
240
|
+
@property
|
|
241
|
+
def _binding_structure(self) -> Any:
|
|
242
|
+
return self._core_pose
|
|
243
|
+
|
|
182
244
|
@classmethod
|
|
183
245
|
def _from_rust(cls, rust_pose: _core.Pose) -> Pose:
|
|
184
246
|
instance = cls.__new__(cls)
|
|
@@ -14,6 +14,19 @@ def test_create_root_frame() -> None:
|
|
|
14
14
|
assert frame.depth == 0
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
def test_tree_structure() -> None:
|
|
18
|
+
frame = Frame("root")
|
|
19
|
+
pos = Position(1.0, 2.0, 3.0)
|
|
20
|
+
quat = Quaternion(0.0, 0.0, 0.0, 1.0)
|
|
21
|
+
child = frame.add_child("child", pos, quat)
|
|
22
|
+
grandchild = child.add_child("grandchild", pos, quat)
|
|
23
|
+
assert grandchild.depth == 2 # noqa: PLR2004
|
|
24
|
+
parent = grandchild.parent()
|
|
25
|
+
assert parent is not None
|
|
26
|
+
assert parent.name == "child"
|
|
27
|
+
assert grandchild.root().name == "root"
|
|
28
|
+
|
|
29
|
+
|
|
17
30
|
def test_add_child_frame_with_quaternion() -> None:
|
|
18
31
|
root = Frame("base")
|
|
19
32
|
pos = Position(1.0, 2.0, 3.0)
|
|
@@ -80,6 +93,12 @@ def test_add_pose_and_update() -> None:
|
|
|
80
93
|
up_pos, _ = pose.transformation()
|
|
81
94
|
assert up_pos.to_tuple() == pytest.approx((4.0, 5.0, 6.0), abs=1e-5)
|
|
82
95
|
|
|
96
|
+
# Access frame
|
|
97
|
+
frame_of_pose = pose.frame()
|
|
98
|
+
assert frame_of_pose.name == "base"
|
|
99
|
+
frame_of_pose.add_child("child_of_pose_frame", pos, quat)
|
|
100
|
+
assert len(frame_of_pose.children()) == 1
|
|
101
|
+
|
|
83
102
|
|
|
84
103
|
def test_pose_in_frame() -> None:
|
|
85
104
|
base = Frame("base")
|
|
@@ -93,3 +112,36 @@ def test_pose_in_frame() -> None:
|
|
|
93
112
|
|
|
94
113
|
assert pos.to_tuple() == pytest.approx((1.0, -3.0, 1.0), abs=1e-5)
|
|
95
114
|
assert quat.to_rpy().to_tuple() == pytest.approx((0.0, 0.0, -radians(90)), abs=1e-5)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_calibrate_frame() -> None:
|
|
118
|
+
base = Frame("base")
|
|
119
|
+
reference_frame = base.add_child("reference", Position(1, 1, 1), Quaternion(0, 0, 0, 1))
|
|
120
|
+
reference_pose = reference_frame.add_pose(Position(1, 1, 1), Quaternion(0, 0, 0, 1))
|
|
121
|
+
|
|
122
|
+
calibrated_frame = base.calibrate_child("calibrated", Position(0, 0, 0), RPY(0, 0, 0), reference_pose)
|
|
123
|
+
|
|
124
|
+
pos, quat = calibrated_frame.transformation_to_parent()
|
|
125
|
+
|
|
126
|
+
assert pos.to_tuple() == pytest.approx((2.0, 2.0, 2.0), abs=1e-5)
|
|
127
|
+
assert quat.to_tuple() == pytest.approx((0.0, 0.0, 0.0, 1.0), abs=1e-5)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_serialization() -> None:
|
|
131
|
+
root = Frame("root")
|
|
132
|
+
child1 = root.add_child("child1", Position(1, 0, 0), Quaternion(0, 0, 0, 1))
|
|
133
|
+
child2 = child1.add_child("child2", Position(0, 1, 0), RPY(0, 0, radians(90)))
|
|
134
|
+
child2.add_pose(Position(0, 0, 1), Quaternion(0, 0, 0, 1))
|
|
135
|
+
|
|
136
|
+
json_str = root.to_json()
|
|
137
|
+
|
|
138
|
+
default_root = Frame("root")
|
|
139
|
+
default_child1 = default_root.add_child("child1", Position(2, 0, 0), Quaternion(0, 0, 0, 1))
|
|
140
|
+
default_child2 = default_child1.add_child("child2", Position(0, 2, 0), RPY(0, 0, radians(90)))
|
|
141
|
+
|
|
142
|
+
default_root.apply_config(json_str)
|
|
143
|
+
|
|
144
|
+
position, _ = default_child1.transformation_to_parent()
|
|
145
|
+
assert position.to_tuple() == pytest.approx((1.0, 0.0, 0.0), abs=1e-5) # Updated back to '1'
|
|
146
|
+
position, _ = default_child2.transformation_to_parent()
|
|
147
|
+
assert position.to_tuple() == pytest.approx((0.0, 1.0, 0.0), abs=1e-5) # Updated back to '1'
|
|
@@ -26,7 +26,7 @@ impl PyFrame {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
#[getter]
|
|
29
|
-
fn name(&self) -> String {
|
|
29
|
+
pub(crate) fn name(&self) -> String {
|
|
30
30
|
self.rust_frame.name()
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -45,6 +45,25 @@ impl PyFrame {
|
|
|
45
45
|
})
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
#[pyo3(signature = (name, desired_position, desired_quaternion, reference_pose))]
|
|
49
|
+
fn calibrate_child(
|
|
50
|
+
&self,
|
|
51
|
+
name: String,
|
|
52
|
+
desired_position: PyPosition,
|
|
53
|
+
desired_quaternion: PyQuaternion,
|
|
54
|
+
reference_pose: &PyPose,
|
|
55
|
+
) -> PyResult<PyFrame> {
|
|
56
|
+
let new_rust_frame = self.rust_frame.calibrate_child(
|
|
57
|
+
name,
|
|
58
|
+
desired_position.position,
|
|
59
|
+
desired_quaternion.quat,
|
|
60
|
+
&reference_pose.rust_pose,
|
|
61
|
+
)?;
|
|
62
|
+
Ok(PyFrame {
|
|
63
|
+
rust_frame: new_rust_frame,
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
48
67
|
#[pyo3(signature = (position, quaternion))]
|
|
49
68
|
fn add_pose(&self, position: PyPosition, quaternion: PyQuaternion) -> PyPose {
|
|
50
69
|
let rust_pose = self.rust_frame.add_pose(position.position, quaternion.quat);
|
|
@@ -74,11 +93,27 @@ impl PyFrame {
|
|
|
74
93
|
Ok(())
|
|
75
94
|
}
|
|
76
95
|
|
|
96
|
+
fn to_json(&self) -> PyResult<String> {
|
|
97
|
+
Ok(self.rust_frame.to_json()?)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#[pyo3(signature = (json))]
|
|
101
|
+
fn apply_config(&self, json: String) -> PyResult<()> {
|
|
102
|
+
self.rust_frame.apply_config(&json)?;
|
|
103
|
+
Ok(())
|
|
104
|
+
}
|
|
105
|
+
|
|
77
106
|
#[getter]
|
|
78
107
|
fn depth(&self) -> usize {
|
|
79
108
|
self.rust_frame.depth()
|
|
80
109
|
}
|
|
81
110
|
|
|
111
|
+
fn root(&self) -> PyFrame {
|
|
112
|
+
PyFrame {
|
|
113
|
+
rust_frame: self.rust_frame.root(),
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
82
117
|
fn parent(&self) -> Option<PyFrame> {
|
|
83
118
|
self.rust_frame
|
|
84
119
|
.parent()
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
use pyo3::prelude::*;
|
|
2
2
|
|
|
3
|
-
// Import your existing Frame, Pose, CartesianTreeError, IntoOrientation
|
|
4
3
|
use crate::{
|
|
5
4
|
Pose as RustPose,
|
|
6
5
|
bindings::{
|
|
@@ -17,9 +16,10 @@ pub struct PyPose {
|
|
|
17
16
|
|
|
18
17
|
#[pymethods]
|
|
19
18
|
impl PyPose {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
fn frame(&self) -> Option<PyFrame> {
|
|
20
|
+
self.rust_pose
|
|
21
|
+
.frame()
|
|
22
|
+
.map(|frame| PyFrame { rust_frame: frame })
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
fn transformation(&self) -> (PyPosition, PyQuaternion) {
|
|
@@ -69,7 +69,9 @@ impl PyPose {
|
|
|
69
69
|
let quaternion = isometry.rotation.coords;
|
|
70
70
|
format!(
|
|
71
71
|
"'{}', ({:.2}, {:.2}, {:.2})({:.4}, {:.4}, {:.4}, {:.4}))",
|
|
72
|
-
self.
|
|
72
|
+
self.frame()
|
|
73
|
+
.map(|frame| frame.name())
|
|
74
|
+
.unwrap_or_else(|| "Unknown".to_string()),
|
|
73
75
|
position.x,
|
|
74
76
|
position.y,
|
|
75
77
|
position.z,
|
|
@@ -14,4 +14,8 @@ pub enum CartesianTreeError {
|
|
|
14
14
|
IsNoAncestor(String, String),
|
|
15
15
|
#[error("Internal error: Weak pointer upgrade failed")]
|
|
16
16
|
WeakUpgradeFailed(),
|
|
17
|
+
#[error("Serialization/Deserialization error: {0}")]
|
|
18
|
+
SerdeError(#[from] serde_json::Error),
|
|
19
|
+
#[error("Tree structure mismatch during config apply: {0}")]
|
|
20
|
+
Mismatch(String),
|
|
17
21
|
}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
use crate::CartesianTreeError;
|
|
2
2
|
use crate::Pose;
|
|
3
3
|
use crate::orientation::IntoOrientation;
|
|
4
|
+
use crate::tree::Walking;
|
|
4
5
|
use crate::tree::{HasChildren, HasParent, NodeEquality};
|
|
5
6
|
|
|
7
|
+
use nalgebra::UnitQuaternion;
|
|
6
8
|
use nalgebra::{Isometry3, Translation3, Vector3};
|
|
7
9
|
use std::cell::RefCell;
|
|
8
10
|
use std::rc::{Rc, Weak};
|
|
9
11
|
|
|
12
|
+
use serde::{Deserialize, Serialize};
|
|
13
|
+
use serde_json;
|
|
14
|
+
|
|
10
15
|
/// Represents a coordinate frame in a Cartesian tree structure.
|
|
11
16
|
///
|
|
12
17
|
/// Each frame can have one parent and multiple children. The frame stores its
|
|
@@ -30,6 +35,14 @@ pub(crate) struct FrameData {
|
|
|
30
35
|
children: Vec<Frame>,
|
|
31
36
|
}
|
|
32
37
|
|
|
38
|
+
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
39
|
+
struct SerialFrame {
|
|
40
|
+
name: String,
|
|
41
|
+
position: Vector3<f64>,
|
|
42
|
+
orientation: UnitQuaternion<f64>,
|
|
43
|
+
children: Vec<SerialFrame>,
|
|
44
|
+
}
|
|
45
|
+
|
|
33
46
|
impl Frame {
|
|
34
47
|
/// Creates a new root frame (origin) with the given name.
|
|
35
48
|
///
|
|
@@ -54,11 +67,11 @@ impl Frame {
|
|
|
54
67
|
}
|
|
55
68
|
}
|
|
56
69
|
|
|
57
|
-
pub(crate) fn borrow(&self) -> std::cell::Ref<FrameData> {
|
|
70
|
+
pub(crate) fn borrow(&self) -> std::cell::Ref<'_, FrameData> {
|
|
58
71
|
self.data.borrow()
|
|
59
72
|
}
|
|
60
73
|
|
|
61
|
-
fn borrow_mut(&self) -> std::cell::RefMut<FrameData> {
|
|
74
|
+
fn borrow_mut(&self) -> std::cell::RefMut<'_, FrameData> {
|
|
62
75
|
self.data.borrow_mut()
|
|
63
76
|
}
|
|
64
77
|
|
|
@@ -217,6 +230,74 @@ impl Frame {
|
|
|
217
230
|
Ok(child)
|
|
218
231
|
}
|
|
219
232
|
|
|
233
|
+
/// Adds a new child frame calibrated such that a reference pose, when expressed in the new frame,
|
|
234
|
+
/// matches the desired position and orientation.
|
|
235
|
+
///
|
|
236
|
+
/// # Arguments
|
|
237
|
+
/// - `name`: The name of the new child frame.
|
|
238
|
+
/// - `desired_position`: The desired position of the reference pose in the new frame.
|
|
239
|
+
/// - `desired_orientation`: The desired orientation of the reference pose in the new frame.
|
|
240
|
+
/// - `reference_pose`: The existing pose (in some frame A) used as the calibration reference.
|
|
241
|
+
///
|
|
242
|
+
/// # Returns
|
|
243
|
+
/// - `Ok(Frame)` the new child frame if successful.
|
|
244
|
+
/// - `Err(CartesianTreeError)` if the reference frame is invalid, no common ancestor exists,
|
|
245
|
+
/// or a child name conflict occurs.
|
|
246
|
+
///
|
|
247
|
+
/// # Example
|
|
248
|
+
/// ```
|
|
249
|
+
/// use cartesian_tree::Frame;
|
|
250
|
+
/// use nalgebra::{Vector3, UnitQuaternion};
|
|
251
|
+
///
|
|
252
|
+
/// let root = Frame::new_origin("root");
|
|
253
|
+
/// let reference_pose = root.add_pose(Vector3::new(1.0, 0.0, 0.0), UnitQuaternion::identity());
|
|
254
|
+
/// let calibrated_child = root.calibrate_child(
|
|
255
|
+
/// "calibrated",
|
|
256
|
+
/// Vector3::zeros(),
|
|
257
|
+
/// UnitQuaternion::identity(),
|
|
258
|
+
/// &reference_pose,
|
|
259
|
+
/// ).unwrap();
|
|
260
|
+
/// ```
|
|
261
|
+
pub fn calibrate_child<O>(
|
|
262
|
+
&self,
|
|
263
|
+
name: impl Into<String>,
|
|
264
|
+
desired_position: Vector3<f64>,
|
|
265
|
+
desired_orientation: O,
|
|
266
|
+
reference_pose: &Pose,
|
|
267
|
+
) -> Result<Frame, CartesianTreeError>
|
|
268
|
+
where
|
|
269
|
+
O: IntoOrientation,
|
|
270
|
+
{
|
|
271
|
+
let reference_frame = reference_pose.frame().ok_or_else(|| {
|
|
272
|
+
CartesianTreeError::FrameDropped("Reference pose frame has been dropped".to_string())
|
|
273
|
+
})?;
|
|
274
|
+
|
|
275
|
+
let ancestor = self.lca_with(&reference_frame).ok_or_else(|| {
|
|
276
|
+
CartesianTreeError::NoCommonAncestor(self.name(), reference_frame.name())
|
|
277
|
+
})?;
|
|
278
|
+
|
|
279
|
+
let t_reference_to_ancestor = reference_frame.walk_up_and_transform(&ancestor)?;
|
|
280
|
+
let t_pose_to_reference = reference_pose.transformation();
|
|
281
|
+
let t_pose_to_ancestor = t_reference_to_ancestor * t_pose_to_reference;
|
|
282
|
+
|
|
283
|
+
let t_parent_to_ancestor = self.walk_up_and_transform(&ancestor)?;
|
|
284
|
+
let t_ancestor_to_parent = t_parent_to_ancestor.inverse();
|
|
285
|
+
|
|
286
|
+
let desired_pose = Isometry3::from_parts(
|
|
287
|
+
Translation3::from(desired_position),
|
|
288
|
+
desired_orientation.into_orientation(),
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
let t_calibrated_to_parent =
|
|
292
|
+
t_pose_to_ancestor * desired_pose.inverse() * t_ancestor_to_parent;
|
|
293
|
+
|
|
294
|
+
self.add_child(
|
|
295
|
+
name,
|
|
296
|
+
t_calibrated_to_parent.translation.vector,
|
|
297
|
+
t_calibrated_to_parent.rotation,
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
|
|
220
301
|
/// Adds a pose to the current frame.
|
|
221
302
|
///
|
|
222
303
|
/// # Arguments
|
|
@@ -240,6 +321,82 @@ impl Frame {
|
|
|
240
321
|
{
|
|
241
322
|
Pose::new(self.downgrade(), position, orientation)
|
|
242
323
|
}
|
|
324
|
+
|
|
325
|
+
/// Serializes the frame tree to a JSON string.
|
|
326
|
+
///
|
|
327
|
+
/// This recursively serializes the hierarchy starting from this frame (ideally the root).
|
|
328
|
+
/// Transforms for root frames are set to identity.
|
|
329
|
+
///
|
|
330
|
+
/// # Returns
|
|
331
|
+
/// - `Ok(String)` the pretty-printed JSON.
|
|
332
|
+
/// - `Err(CartesianTreeError)` on serialization failure.
|
|
333
|
+
pub fn to_json(&self) -> Result<String, CartesianTreeError> {
|
|
334
|
+
let serial = self.to_serial();
|
|
335
|
+
Ok(serde_json::to_string_pretty(&serial)?)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/// Helper function to convert the frame and its children recursively into a serializable structure.
|
|
339
|
+
///
|
|
340
|
+
/// This is used internally for JSON serialization.
|
|
341
|
+
fn to_serial(&self) -> SerialFrame {
|
|
342
|
+
let (position, orientation) = if self.parent().is_some() {
|
|
343
|
+
let iso = self.transform_to_parent().unwrap_or(Isometry3::identity());
|
|
344
|
+
(iso.translation.vector, iso.rotation)
|
|
345
|
+
} else {
|
|
346
|
+
(Vector3::zeros(), UnitQuaternion::identity())
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
SerialFrame {
|
|
350
|
+
name: self.name(),
|
|
351
|
+
position,
|
|
352
|
+
orientation,
|
|
353
|
+
children: self.children().into_iter().map(|c| c.to_serial()).collect(),
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/// Applies a JSON config to this frame tree by updating matching transforms.
|
|
358
|
+
///
|
|
359
|
+
/// Deserializes the JSON to a temporary structure, then recursively updates transforms
|
|
360
|
+
/// where names match (partial apply; ignores unmatched nodes in config).
|
|
361
|
+
/// Skips updating root frames (identity assumed) - assumes this frame is the root.
|
|
362
|
+
///
|
|
363
|
+
/// # Arguments
|
|
364
|
+
/// - `json`: The JSON string to apply.
|
|
365
|
+
///
|
|
366
|
+
/// # Returns
|
|
367
|
+
/// - `Ok(())` if applied successfully (even if partial).
|
|
368
|
+
/// - `Err(CartesianTreeError)` on deserialization or mismatch (e.g., root names differ).
|
|
369
|
+
pub fn apply_config(&self, json: &str) -> Result<(), CartesianTreeError> {
|
|
370
|
+
let serial: SerialFrame = serde_json::from_str(json)?;
|
|
371
|
+
self.apply_serial(&serial)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
fn apply_serial(&self, serial: &SerialFrame) -> Result<(), CartesianTreeError> {
|
|
375
|
+
if self.name() != serial.name {
|
|
376
|
+
return Err(CartesianTreeError::Mismatch(format!(
|
|
377
|
+
"Frame names do not match: {} vs {}",
|
|
378
|
+
self.name(),
|
|
379
|
+
serial.name
|
|
380
|
+
)));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// only update if frame has parent
|
|
384
|
+
if self.parent().is_some() {
|
|
385
|
+
self.update_transform(serial.position, serial.orientation)?;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
for potential_child in &serial.children {
|
|
389
|
+
if let Some(child) = self
|
|
390
|
+
.children()
|
|
391
|
+
.into_iter()
|
|
392
|
+
.find(|c| c.name() == potential_child.name)
|
|
393
|
+
{
|
|
394
|
+
child.apply_serial(potential_child)?;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
Ok(())
|
|
399
|
+
}
|
|
243
400
|
}
|
|
244
401
|
|
|
245
402
|
impl HasParent for Frame {
|
|
@@ -403,7 +560,7 @@ mod tests {
|
|
|
403
560
|
let frame = Frame::new_origin("dummy");
|
|
404
561
|
let pose = frame.add_pose(Vector3::new(1.0, 2.0, 3.0), UnitQuaternion::identity());
|
|
405
562
|
|
|
406
|
-
assert_eq!(pose.
|
|
563
|
+
assert_eq!(pose.frame().unwrap().name(), "dummy");
|
|
407
564
|
}
|
|
408
565
|
|
|
409
566
|
#[test]
|
|
@@ -459,4 +616,131 @@ mod tests {
|
|
|
459
616
|
// Total offset should be: f2 (0,2,0) + pose (1,1,0) + f1 (1,0,0)
|
|
460
617
|
assert!((pos - Vector3::new(2.0, 3.0, 0.0)).norm() < 1e-6);
|
|
461
618
|
}
|
|
619
|
+
|
|
620
|
+
#[test]
|
|
621
|
+
fn test_calibrate_child() {
|
|
622
|
+
let root = Frame::new_origin("root");
|
|
623
|
+
|
|
624
|
+
let reference_pose = root.add_pose(
|
|
625
|
+
Vector3::new(1.0, 2.0, 3.0),
|
|
626
|
+
UnitQuaternion::from_euler_angles(0.0, 0.0, std::f64::consts::FRAC_PI_2),
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
// Calibrate a child where the reference pose should appear at (0,0,0) with identity orientation.
|
|
630
|
+
let calibrated_frame = root
|
|
631
|
+
.calibrate_child(
|
|
632
|
+
"calibrated",
|
|
633
|
+
Vector3::zeros(),
|
|
634
|
+
UnitQuaternion::identity(),
|
|
635
|
+
&reference_pose,
|
|
636
|
+
)
|
|
637
|
+
.unwrap();
|
|
638
|
+
|
|
639
|
+
let pose_in_calibrated = reference_pose.in_frame(&calibrated_frame).unwrap();
|
|
640
|
+
let transformation = pose_in_calibrated.transformation();
|
|
641
|
+
|
|
642
|
+
assert!((transformation.translation.vector - Vector3::zeros()).norm() < 1e-6);
|
|
643
|
+
assert!((transformation.rotation.angle() - 0.0).abs() < 1e-6);
|
|
644
|
+
|
|
645
|
+
// Verify the child's transform matches the reference pose's original transform.
|
|
646
|
+
let calibrated_transformation = calibrated_frame.transform_to_parent().unwrap();
|
|
647
|
+
assert!(
|
|
648
|
+
(calibrated_transformation.translation.vector - Vector3::new(1.0, 2.0, 3.0)).norm()
|
|
649
|
+
< 1e-6
|
|
650
|
+
);
|
|
651
|
+
assert!(
|
|
652
|
+
(calibrated_transformation.rotation.angle() - std::f64::consts::FRAC_PI_2).abs() < 1e-6
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
#[test]
|
|
657
|
+
fn test_to_json_and_apply_config() {
|
|
658
|
+
let root = Frame::new_origin("root");
|
|
659
|
+
let _ = root
|
|
660
|
+
.add_child(
|
|
661
|
+
"child",
|
|
662
|
+
Vector3::new(1.0, 2.0, 3.0),
|
|
663
|
+
UnitQuaternion::from_euler_angles(0.1, 0.2, 0.3),
|
|
664
|
+
)
|
|
665
|
+
.unwrap();
|
|
666
|
+
|
|
667
|
+
let json = root.to_json().unwrap();
|
|
668
|
+
// roughly verify JSON structure
|
|
669
|
+
assert!(json.contains(r#""name": "root""#));
|
|
670
|
+
assert!(json.contains(r#""name": "child""#));
|
|
671
|
+
|
|
672
|
+
// Create a default tree with different transforms
|
|
673
|
+
let default_root = Frame::new_origin("root");
|
|
674
|
+
default_root
|
|
675
|
+
.add_child(
|
|
676
|
+
"child",
|
|
677
|
+
Vector3::new(0.0, 0.0, 0.0),
|
|
678
|
+
UnitQuaternion::identity(),
|
|
679
|
+
)
|
|
680
|
+
.unwrap();
|
|
681
|
+
|
|
682
|
+
// Apply config
|
|
683
|
+
default_root.apply_config(&json).unwrap();
|
|
684
|
+
|
|
685
|
+
// Verify child transform updated
|
|
686
|
+
let updated_child = default_root
|
|
687
|
+
.children()
|
|
688
|
+
.into_iter()
|
|
689
|
+
.find(|c| c.name() == "child")
|
|
690
|
+
.unwrap();
|
|
691
|
+
let iso = updated_child.transform_to_parent().unwrap();
|
|
692
|
+
assert_eq!(iso.translation.vector, Vector3::new(1.0, 2.0, 3.0));
|
|
693
|
+
let (r, p, y) = iso.rotation.euler_angles();
|
|
694
|
+
assert!((r - 0.1).abs() < 1e-6);
|
|
695
|
+
assert!((p - 0.2).abs() < 1e-6);
|
|
696
|
+
assert!((y - 0.3).abs() < 1e-6);
|
|
697
|
+
|
|
698
|
+
// Test partial: If config has extra, ignore it
|
|
699
|
+
let partial_json = r#"
|
|
700
|
+
{
|
|
701
|
+
"name": "root",
|
|
702
|
+
"position": [0.0, 0.0, 0.0],
|
|
703
|
+
"orientation": [0.0, 0.0, 0.0, 1.0],
|
|
704
|
+
"children": [
|
|
705
|
+
{
|
|
706
|
+
"name": "child",
|
|
707
|
+
"position": [4.0, 5.0, 6.0],
|
|
708
|
+
"orientation": [0.0, 0.0, 0.0, 1.0],
|
|
709
|
+
"children": []
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
"name": "extra",
|
|
713
|
+
"position": [0.0, 0.0, 0.0],
|
|
714
|
+
"orientation": [0.0, 0.0, 0.0, 1.0],
|
|
715
|
+
"children": []
|
|
716
|
+
}
|
|
717
|
+
]
|
|
718
|
+
}
|
|
719
|
+
"#;
|
|
720
|
+
default_root.apply_config(partial_json).unwrap();
|
|
721
|
+
let updated_child = default_root
|
|
722
|
+
.children()
|
|
723
|
+
.into_iter()
|
|
724
|
+
.find(|c| c.name() == "child")
|
|
725
|
+
.unwrap();
|
|
726
|
+
assert_eq!(
|
|
727
|
+
updated_child
|
|
728
|
+
.transform_to_parent()
|
|
729
|
+
.unwrap()
|
|
730
|
+
.translation
|
|
731
|
+
.vector,
|
|
732
|
+
Vector3::new(4.0, 5.0, 6.0)
|
|
733
|
+
);
|
|
734
|
+
|
|
735
|
+
// Test mismatch
|
|
736
|
+
let mismatch_json = r#"
|
|
737
|
+
{
|
|
738
|
+
"name": "wrong_root",
|
|
739
|
+
"position": [0.0, 0.0, 0.0],
|
|
740
|
+
"orientation": [0.0, 0.0, 0.0, 1.0],
|
|
741
|
+
"children": []
|
|
742
|
+
}
|
|
743
|
+
"#;
|
|
744
|
+
assert!(default_root.apply_config(mismatch_json).is_err());
|
|
745
|
+
}
|
|
462
746
|
}
|
|
@@ -23,7 +23,7 @@ use pyo3::prelude::*;
|
|
|
23
23
|
|
|
24
24
|
#[cfg(feature = "bindings")]
|
|
25
25
|
#[pymodule]
|
|
26
|
-
#[pyo3(name = "
|
|
26
|
+
#[pyo3(name = "_cartesian_tree")]
|
|
27
27
|
fn cartesian_tree(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
28
28
|
m.add_class::<bindings::frame::PyFrame>()?;
|
|
29
29
|
m.add_class::<bindings::pose::PyPose>()?;
|
|
@@ -37,27 +37,23 @@ impl Pose {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/// Returns the
|
|
40
|
+
/// Returns the parent frame of this pose.
|
|
41
41
|
///
|
|
42
42
|
/// # Returns
|
|
43
|
-
///
|
|
44
|
-
/// `Some(String)` if the parent frame is still valid, or `None` if the frame
|
|
43
|
+
/// `Some(Frame)` if the parent frame is still valid, or `None` if the frame
|
|
45
44
|
/// has been dropped or no longer exists.
|
|
46
45
|
///
|
|
47
46
|
/// # Example
|
|
48
|
-
///
|
|
49
47
|
/// ```
|
|
50
48
|
/// use cartesian_tree::Frame;
|
|
51
49
|
/// use nalgebra::{Vector3, UnitQuaternion};
|
|
52
50
|
///
|
|
53
51
|
/// let frame = Frame::new_origin("base");
|
|
54
52
|
/// let pose = frame.add_pose(Vector3::new(0.0, 0.0, 0.0), UnitQuaternion::identity());
|
|
55
|
-
/// assert_eq!(pose.
|
|
53
|
+
/// assert_eq!(pose.frame().unwrap().name(), "base");
|
|
56
54
|
/// ```
|
|
57
|
-
pub fn
|
|
58
|
-
self.parent
|
|
59
|
-
.upgrade()
|
|
60
|
-
.map(|frame| frame.borrow().name.clone())
|
|
55
|
+
pub fn frame(&self) -> Option<Frame> {
|
|
56
|
+
self.parent.upgrade().map(|data| Frame { data })
|
|
61
57
|
}
|
|
62
58
|
|
|
63
59
|
/// Returns the transformation from this pose to its parent frame.
|
|
@@ -54,6 +54,15 @@ pub trait Walking: HasParent<Node = Self> + NodeEquality + Clone {
|
|
|
54
54
|
Some(current)
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/// Finds the root of this node.
|
|
58
|
+
///
|
|
59
|
+
/// # Returns
|
|
60
|
+
///
|
|
61
|
+
/// The root Node.
|
|
62
|
+
fn root(&self) -> Self {
|
|
63
|
+
self.walk_up(self.depth()).unwrap_or_else(|| self.clone())
|
|
64
|
+
}
|
|
65
|
+
|
|
57
66
|
/// Finds the lowest common ancestor with another Node.
|
|
58
67
|
///
|
|
59
68
|
/// # Arguments.
|
|
@@ -126,6 +135,21 @@ mod tests {
|
|
|
126
135
|
assert!(grandchild.walk_up(3).is_none());
|
|
127
136
|
}
|
|
128
137
|
|
|
138
|
+
#[test]
|
|
139
|
+
fn test_root() {
|
|
140
|
+
let root = Frame::new_origin("root");
|
|
141
|
+
let child = root
|
|
142
|
+
.add_child("child", Vector3::zeros(), UnitQuaternion::identity())
|
|
143
|
+
.unwrap();
|
|
144
|
+
let grandchild = child
|
|
145
|
+
.add_child("grandchild", Vector3::zeros(), UnitQuaternion::identity())
|
|
146
|
+
.unwrap();
|
|
147
|
+
|
|
148
|
+
assert!(root.root().is_same(&root));
|
|
149
|
+
assert!(child.root().is_same(&root));
|
|
150
|
+
assert!(grandchild.root().is_same(&root));
|
|
151
|
+
}
|
|
152
|
+
|
|
129
153
|
#[test]
|
|
130
154
|
fn test_lca_with() {
|
|
131
155
|
let root = Frame::new_origin("root");
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
name: Publish
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
workflow_call:
|
|
5
|
-
secrets:
|
|
6
|
-
CARGO_REGISTRY_TOKEN:
|
|
7
|
-
required: true
|
|
8
|
-
PYPI_TOKEN:
|
|
9
|
-
required: true
|
|
10
|
-
|
|
11
|
-
env:
|
|
12
|
-
CARGO_TERM_COLOR: always
|
|
13
|
-
|
|
14
|
-
jobs:
|
|
15
|
-
publish-rust:
|
|
16
|
-
name: Publish Rust Crate
|
|
17
|
-
runs-on: ubuntu-latest
|
|
18
|
-
steps:
|
|
19
|
-
- uses: actions/checkout@v4
|
|
20
|
-
- name: Install Rust toolchain
|
|
21
|
-
uses: dtolnay/rust-toolchain@stable
|
|
22
|
-
|
|
23
|
-
- name: Publish to crates.io
|
|
24
|
-
run: cargo publish --token ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
|
25
|
-
|
|
26
|
-
publish-python:
|
|
27
|
-
name: Publish Python Package
|
|
28
|
-
runs-on: ubuntu-latest
|
|
29
|
-
steps:
|
|
30
|
-
- uses: actions/checkout@v4
|
|
31
|
-
- name: Setup Python
|
|
32
|
-
uses: actions/setup-python@v5
|
|
33
|
-
with:
|
|
34
|
-
python-version: "3.x"
|
|
35
|
-
|
|
36
|
-
- name: Install maturin
|
|
37
|
-
run: pip install maturin
|
|
38
|
-
|
|
39
|
-
- name: Build and publish to PyPI
|
|
40
|
-
run: maturin publish --username __token__ --password ${{ secrets.PYPI_TOKEN }}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|