Skip to content

Commit cd3a268

Browse files
authored
feat(msgs): add PointStamped geometry message type (#1388)
* feat(msgs): add PointStamped geometry message type adds geometry_msgs.PointStamped following the PoseStamped pattern: - inherits from dimos_lcm PointStamped + Timestamped mixin - lcm_encode/lcm_decode for binary roundtrip - to_rerun() → rr.Points3D - to_pose_stamped() → PoseStamped with identity orientation - plum.dispatch constructors (x/y/z, Vector3, list, kwargs) - conforms to DimosMsg protocol - 30 unit tests enables the rerun viewer click-to-navigate pipeline: viewer publishes PointStamped to /clicked_point via LCM, any module subscribes via LCMTransport and converts to PoseStamped for navigation. DIM-643 * CI code cleanup * refactor(msgs): drop plum.dispatch, use Point(LCMPoint) base - replace multi-dispatch constructors with single __init__(x, y, z, ts, frame_id) - inherit from Point(LCMPoint) instead of Vector3(LCMVector3) - add Point wrapper class for geometry_msgs.Point - simplify tests to focus on LCM roundtrip per review feedback addresses review comments from leshy on PR #1388 * test(msgs): simplify PointStamped tests to match TwistStamped pattern 3 focused tests: lcm roundtrip, Point inheritance, PoseStamped conversion * style(msgs): ruff format and lint fixes * refactor(msgs): use Timestamped.ros_timestamp() instead of local sec_nsec * refactor(msgs): move Point to separate file Point is a separate LCM message type, should have its own file like Vector3, Quaternion, Pose, etc. * fix(msgs): resolve mypy errors in Point/PointStamped - add type: ignore[misc] for LCMPoint subclass (generated code has Any type) - add PoseStamped to TYPE_CHECKING import block - fix test import: Point from Point module, not PointStamped module --------- Co-authored-by: spomichter <12108168+spomichter@users.noreply.github.com>
1 parent 0d58b95 commit cd3a268

4 files changed

Lines changed: 203 additions & 0 deletions

File tree

dimos/msgs/geometry_msgs/Point.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Copyright 2025-2026 Dimensional Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
from dimos_lcm.geometry_msgs import Point as LCMPoint
18+
19+
20+
class Point(LCMPoint): # type: ignore[misc]
21+
"""DimOS wrapper for geometry_msgs.Point (3D position).
22+
23+
Inherits x/y/z from LCMPoint. Wire-identical to Vector3 but
24+
semantically represents a position, not a direction/displacement.
25+
"""
26+
27+
msg_name = "geometry_msgs.Point"
28+
29+
def __init__(self, x: float = 0.0, y: float = 0.0, z: float = 0.0) -> None:
30+
super().__init__(float(x), float(y), float(z))
31+
32+
def __repr__(self) -> str:
33+
return f"Point(x={self.x}, y={self.y}, z={self.z})"
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Copyright 2025-2026 Dimensional Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
import time
18+
from typing import TYPE_CHECKING, BinaryIO
19+
20+
if TYPE_CHECKING:
21+
from rerun._baseclasses import Archetype
22+
23+
from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped
24+
25+
from dimos_lcm.geometry_msgs import PointStamped as LCMPointStamped
26+
27+
from dimos.msgs.geometry_msgs.Point import Point
28+
from dimos.types.timestamped import Timestamped
29+
30+
31+
class PointStamped(Point, Timestamped):
32+
"""A 3D point with timestamp and frame_id.
33+
34+
Follows the same pattern as PoseStamped(Pose, Timestamped) and
35+
TwistStamped(Twist, Timestamped). Inherits x/y/z from Point
36+
(which inherits from LCMPoint).
37+
"""
38+
39+
msg_name = "geometry_msgs.PointStamped"
40+
ts: float
41+
frame_id: str
42+
43+
def __init__(
44+
self,
45+
x: float = 0.0,
46+
y: float = 0.0,
47+
z: float = 0.0,
48+
ts: float = 0.0,
49+
frame_id: str = "",
50+
) -> None:
51+
self.frame_id = frame_id
52+
self.ts = ts if ts != 0 else time.time()
53+
super().__init__(float(x), float(y), float(z))
54+
55+
# -- LCM encode / decode --
56+
57+
def lcm_encode(self) -> bytes:
58+
"""Encode to LCM binary format."""
59+
lcm_msg = LCMPointStamped()
60+
lcm_msg.point = self # Works because Point inherits from LCMPoint
61+
[lcm_msg.header.stamp.sec, lcm_msg.header.stamp.nsec] = self.ros_timestamp()
62+
lcm_msg.header.frame_id = self.frame_id
63+
return lcm_msg.lcm_encode() # type: ignore[no-any-return]
64+
65+
@classmethod
66+
def lcm_decode(cls, data: bytes | BinaryIO) -> PointStamped:
67+
"""Decode from LCM binary format."""
68+
lcm_msg = LCMPointStamped.lcm_decode(data)
69+
return cls(
70+
x=lcm_msg.point.x,
71+
y=lcm_msg.point.y,
72+
z=lcm_msg.point.z,
73+
ts=lcm_msg.header.stamp.sec + (lcm_msg.header.stamp.nsec / 1_000_000_000),
74+
frame_id=lcm_msg.header.frame_id,
75+
)
76+
77+
# -- Conversion methods --
78+
79+
def to_rerun(self) -> Archetype:
80+
"""Convert to rerun Points3D archetype for visualization."""
81+
import rerun as rr
82+
83+
return rr.Points3D(positions=[[self.x, self.y, self.z]])
84+
85+
def to_pose_stamped(self) -> PoseStamped:
86+
"""Convert to PoseStamped with identity quaternion orientation."""
87+
from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped
88+
89+
return PoseStamped(
90+
ts=self.ts,
91+
frame_id=self.frame_id,
92+
position=[self.x, self.y, self.z],
93+
orientation=[0.0, 0.0, 0.0, 1.0],
94+
)
95+
96+
# -- String representations --
97+
98+
def __str__(self) -> str:
99+
return f"PointStamped(point=[{self.x:.3f}, {self.y:.3f}, {self.z:.3f}], frame_id={self.frame_id!r})"
100+
101+
def __repr__(self) -> str:
102+
return f"PointStamped(x={self.x}, y={self.y}, z={self.z}, ts={self.ts}, frame_id={self.frame_id!r})"

dimos/msgs/geometry_msgs/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from dimos.msgs.geometry_msgs.Point import Point
2+
from dimos.msgs.geometry_msgs.PointStamped import PointStamped
13
from dimos.msgs.geometry_msgs.Pose import Pose, PoseLike, to_pose
24
from dimos.msgs.geometry_msgs.PoseArray import PoseArray
35
from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped
@@ -14,6 +16,8 @@
1416
from dimos.msgs.geometry_msgs.WrenchStamped import WrenchStamped
1517

1618
__all__ = [
19+
"Point",
20+
"PointStamped",
1721
"Pose",
1822
"PoseArray",
1923
"PoseLike",
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Copyright 2025-2026 Dimensional Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Tests for geometry_msgs.PointStamped — msg -> lcm bytes -> msg roundtrip."""
16+
17+
import time
18+
19+
from dimos_lcm.geometry_msgs import Point as LCMPoint
20+
21+
from dimos.msgs.geometry_msgs.Point import Point
22+
from dimos.msgs.geometry_msgs.PointStamped import PointStamped
23+
24+
25+
def test_point_inherits_lcm() -> None:
26+
"""Point wrapper inherits from LCMPoint."""
27+
assert isinstance(Point(1.0, 2.0, 3.0), LCMPoint)
28+
29+
30+
def test_lcm_encode_decode() -> None:
31+
"""Test encoding and decoding of PointStamped to/from binary LCM format."""
32+
source = PointStamped(
33+
x=1.5,
34+
y=-2.5,
35+
z=3.5,
36+
ts=time.time(),
37+
frame_id="/world/grid",
38+
)
39+
binary_msg = source.lcm_encode()
40+
dest = PointStamped.lcm_decode(binary_msg)
41+
42+
assert isinstance(dest, PointStamped)
43+
assert dest is not source
44+
assert dest.x == source.x
45+
assert dest.y == source.y
46+
assert dest.z == source.z
47+
assert abs(dest.ts - source.ts) < 1e-6
48+
assert dest.frame_id == source.frame_id
49+
50+
51+
def test_to_pose_stamped() -> None:
52+
"""Test conversion to PoseStamped with identity orientation."""
53+
from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped
54+
55+
pt = PointStamped(x=1.0, y=2.0, z=3.0, ts=500.0, frame_id="/map")
56+
pose = pt.to_pose_stamped()
57+
58+
assert isinstance(pose, PoseStamped)
59+
assert pose.x == 1.0
60+
assert pose.y == 2.0
61+
assert pose.z == 3.0
62+
assert pose.orientation.w == 1.0
63+
assert pose.ts == 500.0
64+
assert pose.frame_id == "/map"

0 commit comments

Comments
 (0)