Skip to content

Add ability to rearrange axes#52

Open
nickswalker wants to merge 4 commits into
JakubAndrysek:masterfrom
nickswalker:nick/unity-ros-conventions
Open

Add ability to rearrange axes#52
nickswalker wants to merge 4 commits into
JakubAndrysek:masterfrom
nickswalker:nick/unity-ros-conventions

Conversation

@nickswalker

@nickswalker nickswalker commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Common alternative axis conventions (ROS, Unity #50 (comment)) would require swapping axes (not merely inverting them). This wasn't supported, so this PR adds that ability. Not 100% sure on this design but figure you may already be thinking about it

apply_axis_convention() generalized. Previously only accepted LEGACY specs as input. Now converts between any pair of named conventions via an internal HID normalization step. DeviceInfo gained a convention field (default AxisConvention.LEGACY) to track this.

modify_device_info() extended. New remap_axes: AxisRemap parameter for axis swaps and sign flips in one step. remap_axes and invert_axes are mutually exclusive. Returns convention=None when axes are modified, preserves the input convention (newly tracked on the object, per above) otherwise.

New:

  • AxisConvention.ROS, AxisConvention.UNITY
  • AxisRemap type alias
  • modify_device_info(remap_axes=...) new parameter
  • create_device_info(convention=...) new parameter
  • DeviceInfo.convention new field (default AxisConvention.LEGACY, backward compatible)

Summary by Sourcery

Add support for named axis conventions and flexible axis remapping to better match common coordinate frames such as ROS and Unity.

New Features:

  • Allow converting DeviceInfo specs between any named axis convention via HID normalization in apply_axis_convention().
  • Introduce AxisRemap-based axis remapping and explicit inversion in modify_device_info() for swapping and flipping axes in one step.
  • Add ROS and Unity axis conventions and track the axis convention directly on DeviceInfo, including via a new convention parameter on create_device_info().

Enhancements:

  • Generalize axis_convention usage in the public API to cover multiple application-specific frames instead of only HID/HID_Z_UP.
  • Improve validation and error handling around axis remapping and inversion operations.

Documentation:

  • Document the new ROS and Unity axis conventions in the mouse API reference.
  • Update README and troubleshooting guides to show using axis_convention and the new remap_axes parameter for custom coordinate frames.

apply_axis_convention converts between any named conventions (normalize to HID first)
modify_device_info gains remap_axes; mutually exclusive with invert_axes
# Conflicts:
#	docs/mouseApi/examples.md
@sourcery-ai

sourcery-ai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds explicit axis remapping support and tracks axis conventions on DeviceInfo, generalizing convention conversion to work between multiple named frames (LEGACY, HID, HID_Z_UP, ROS, UNITY) while updating the public API, docs, and examples to expose ROS/Unity and the new remap_axes parameter.

Sequence diagram for generalized axis convention conversion

sequenceDiagram
    participant caller as apply_axis_convention
    participant normalizer as _normalize_to_hid
    participant remaps as _HID_TO_CONVENTION
    participant parser as _parse_axis_remap
    participant copier as _copy_axis_spec

    caller->>normalizer: _normalize_to_hid(base)
    normalizer-->>caller: hid_base
    caller->>remaps: read _HID_TO_CONVENTION[convention]
    loop for each target_axis, src in remap
        caller->>parser: _parse_axis_remap(target_axis, src)
        parser-->>caller: source_axis, sign
        caller->>copier: _copy_axis_spec(hid_base.mappings[source_axis], sign)
        copier-->>caller: AxisSpec
    end
    caller-->>caller: construct DeviceInfo with convention=convention
Loading

File-Level Changes

Change Details Files
Introduce axis remapping infrastructure and generalized convention conversion around a HID-normalized internal representation.
  • Define AxisRemap type alias plus helper functions for parsing remaps and copying AxisSpec with sign handling.
  • Add a HID-to-convention remap table and a _normalize_to_hid() helper to convert any named DeviceInfo convention to HID.
  • Refactor apply_axis_convention() to normalize input to HID, then apply HID→target remaps for any named AxisConvention, returning specs with convention set to the target.
pyspacemouse/config_helpers.py
Extend DeviceInfo construction and mutation to carry a convention and support atomic axis remapping and inversion, invalidating convention when axes change.
  • Add convention parameter to create_device_info(), thread it into the constructed DeviceInfo, and document it in the function docstring.
  • Extend modify_device_info() to accept remap_axes with validation, make invert_axes use common helpers, enforce mutual exclusion between remap_axes and invert_axes, and set DeviceInfo.convention to None when axes are modified while preserving it otherwise.
  • Introduce DeviceInfo.convention field with default AxisConvention.LEGACY in the dataclass definition to track the spec’s named convention.
pyspacemouse/config_helpers.py
pyspacemouse/types.py
Expose new ROS and Unity conventions and update API/docs to show how to use axis_convention and remap_axes for common and custom frames.
  • Add AxisConvention.ROS and AxisConvention.UNITY enum members with docstrings describing their translation and rotation frames.
  • Update open_by_path() and open() docstrings and deprecation warning text to mention ROS and Unity conventions alongside HID/HID_Z_UP.
  • Revise README and docs examples to demonstrate axis_convention usage for ROS and remap_axes-based custom mappings instead of simple invert_axes-only patterns.
pyspacemouse/api.py
README.md
docs/README.md
docs/mouseApi/index.md
docs/troubleshooting.md
pyspacemouse/types.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="pyspacemouse/config_helpers.py" line_range="70-71" />
<code_context>
+}
+
+
+def _parse_axis_remap(
+    target_axis: str, remap: Axis | tuple[Axis, Literal[-1, 1]]
+) -> tuple[Axis, int]:
+    if target_axis not in AXIS_NAMES:
</code_context>
<issue_to_address>
**issue (bug_risk):** The use of `|` union syntax in type hints will prevent this module from importing on Python 3.8.

Because `Axis | tuple[...]` uses PEP 604 syntax, this function annotation will cause a SyntaxError on Python 3.8 at import time. For 3.8 compatibility, update this parameter type to `Union[Axis, Tuple[Axis, Literal[-1, 1]]]` (to align with `AxisRemap`) or remove the annotation.
</issue_to_address>

### Comment 2
<location path="pyspacemouse/config_helpers.py" line_range="119-121" />
<code_context>
+    # Invert HID→C to get C→HID:
+    # HID[hid_target] = C[c_source] * sign  →  C[c_source] = HID[hid_target] * sign
+    new_mappings = {}
+    for hid_target, src in _HID_TO_CONVENTION[spec.convention].items():
+        c_source, sign = _parse_axis_remap(hid_target, src)
+        new_mappings[hid_target] = _copy_axis_spec(spec.mappings[c_source], sign)
+
+    return DeviceInfo(
</code_context>
<issue_to_address>
**issue (bug_risk):** Normalizing to HID assumes all source axes exist, which can raise a KeyError for partially defined specs.

`spec.mappings[c_source]` will raise if a source axis is missing, whereas the previous `apply_axis_convention` handled missing axes. If partially defined specs are expected (e.g., rotation-only/translation-only devices), consider skipping entries when `c_source` is absent or raising a controlled `ValueError` instead of letting a KeyError bubble up.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread pyspacemouse/config_helpers.py
Comment thread pyspacemouse/config_helpers.py
@peter-mitrano-ar peter-mitrano-ar self-requested a review July 2, 2026 19:00

@peter-mitrano-ar peter-mitrano-ar left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the future please add me as a reviewer, I just noticed this by chance! Anyways, looks mostly good to me. I still need to try it myself manually though before I would merge. But I am posting one or two comments/suggestions now and will come back to it later

Comment thread pyspacemouse/types.py Outdated
Comment thread pyspacemouse/config_helpers.py Outdated
Comment on lines +35 to +67
AxisConvention.LEGACY: {
"x": ("x", 1),
"y": ("y", -1),
"z": ("z", -1),
"roll": ("pitch", -1),
"pitch": ("roll", -1),
"yaw": ("yaw", 1),
},
AxisConvention.HID_Z_UP: {
"x": ("x", 1),
"y": ("y", -1),
"z": ("z", -1),
"roll": ("roll", 1),
"pitch": ("pitch", -1),
"yaw": ("yaw", -1),
},
AxisConvention.ROS: {
"x": ("y", -1),
"y": ("x", -1),
"z": ("z", -1),
"roll": ("pitch", -1),
"pitch": ("roll", -1),
"yaw": ("yaw", -1),
},
AxisConvention.UNITY: {
"x": ("x", 1),
"y": ("z", -1),
"z": ("y", -1),
"roll": ("roll", -1),
"pitch": ("yaw", 1),
"yaw": ("pitch", 1),
},
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks deliciously unit-testable, you could convert from each convention to HID and back and check for self-cosistency, or from HID_Z_UP --> ROS --> HID_Z_UP and such. Would be great to add some unit tests to this code base at some point. Doesn't even have to be run in CI to start if that complicates things. If you have the time/energy to add this let me know, regardless we can merge once I've tested by hand.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went ahead and initialized some in this follow on PR #54



def _parse_axis_remap(
target_axis: str, remap: Axis | tuple[Axis, Literal[-1, 1]]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
target_axis: str, remap: Axis | tuple[Axis, Literal[-1, 1]]
target_axis: str,
remap: Axis | tuple[Axis, Literal[-1, 1]]

I can't believe the linter is happy with this 🫣
hehe just my opinion I guess

nickswalker and others added 2 commits July 7, 2026 13:16
Co-authored-by: Peter Mitrano (AR) <peter.mitrano@agile-robots.com>
Co-authored-by: Peter Mitrano (AR) <peter.mitrano@agile-robots.com>
Comment thread pyspacemouse/api.py
"and will be removed in a future release. Pass "
"axis_convention=AxisConvention.HID_Z_UP for the recommended "
"right-handed Z-up frame, or AxisConvention.HID for raw HID axes.",
"right-handed Z-up frame, other values on AxisConvention for application-specific frames, or "

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to ignore this, as this is at the discretion of the maintainers, but why would HID_Z_UP be the recommended frame when there are multiple to choose from?
I would expect plain HID (unchanged and standardized, what is sent over the wire) as the recommendation when no specific application is "closer" associated with the library. Or simply give no recommendation to begin with.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree! I think this library inherited the odd legacy convention from the pyspacenavigator implementation, and in some sense the correct version of what it was trying to do is HID Z Up: it has the same translation axes just matched with conventional rotation axes.

But if there's an opportunity to reconsider, providing the device's native axes by default feels more neutral/parsimonious. If someone had written code against the raw device they wouldn't have to think much when swapping to use the library. Someone writing fresh code against the library is likely going to think about frame conventions on an application-specific basis, and will flip to the correct one in any case.

Thoughts @JakubAndrysek?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants