From 99307307d61aa9025109cddf0ff2451e8a3895c0 Mon Sep 17 00:00:00 2001 From: ChickenisLegit Date: Sat, 8 Aug 2026 12:37:17 +0530 Subject: [PATCH 1/4] feat: make Waypoints behave like a list (#430) --- src/compas_fab/robots/targets.py | 33 ++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/compas_fab/robots/targets.py b/src/compas_fab/robots/targets.py index 91005844b..7cbf146b0 100644 --- a/src/compas_fab/robots/targets.py +++ b/src/compas_fab/robots/targets.py @@ -728,6 +728,39 @@ class Waypoints(Target): def __init__(self, target_mode: TargetMode = None, native_scale: float = 1.0, name: str = "Generic Waypoints"): super(Waypoints, self).__init__(target_mode=target_mode, native_scale=native_scale, name=name) + @property + def waypoints(self): + if hasattr(self, "target_frames"): + return self.target_frames + elif hasattr(self, "target_points_and_axes"): + return self.target_points_and_axes + else: + raise NotImplementedError + + def __len__(self): + return len(self.waypoints) + + def __getitem__(self, item): + return self.waypoints[item] + + def __setitem__(self, key, value): + self.waypoints[key] = value + + def __delitem__(self, key): + del self.waypoints[key] + + def __iter__(self): + return iter(self.waypoints) + + def append(self, item): + self.waypoints.append(item) + + def extend(self, items): + self.waypoints.extend(items) + + def insert(self, i, item): + self.waypoints.insert(i, item) + class FrameWaypoints(Waypoints): """Represents a sequence of fully constrained pose target for the robot's end-effector using a [`Frame`][compas.geometry.Frame]. From 4aa97a0c7c04509376785438a5064af06c934f31 Mon Sep 17 00:00:00 2001 From: ChickenisLegit Date: Sat, 8 Aug 2026 20:04:24 +0530 Subject: [PATCH 2/4] chore: add changelog entry, unit test for waypoints list behavior, fix pr checks --- .github/workflows/pr-checks.yml | 3 +-- CHANGELOG.md | 1 + tests/robots/test_waypoints.py | 21 +++++++++++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 tests/robots/test_waypoints.py diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 7e9ce2b21..22ff9acf1 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -13,9 +13,8 @@ jobs: steps: - uses: actions/checkout@v6 - name: Changelog check - uses: Zomzog/changelog-checker@v1.1.0 + uses: Zomzog/changelog-checker@v1.3.0 with: fileName: CHANGELOG.md - checkNotification: Simple env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 988249e3f..1bc9cb4ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Requires `compas_robots >= 1.1`. ### Added +* Made `Waypoints` (`FrameWaypoints` and `PointAxisWaypoints`) behave like a list. * The `Tool From Mesh` Grasshopper component gained a `base_plane` input: where the robot's flange takes hold of the geometry, expressed in the coordinates the mesh was modelled in. Its Z axis points away from the robot, so a tool drawn reaching along world Z needs none, and a tool drawn along another axis is mounted by wiring a plane instead of redrawing the geometry. Backed by the new `base_frame` argument of `compas_robots.ToolModel`; nothing is baked into the mesh, so the plane can be re-wired at any time. The component also surfaces a remark when the TCP does not sit roughly on the tool's +Z, since that means the tool will point sideways once attached — the direction from the mount to the TCP is only a hint (it says nothing about roll), so it is reported rather than applied. ### Changed diff --git a/tests/robots/test_waypoints.py b/tests/robots/test_waypoints.py new file mode 100644 index 000000000..62e389420 --- /dev/null +++ b/tests/robots/test_waypoints.py @@ -0,0 +1,21 @@ +import pytest +from compas.geometry import Frame, Point, Vector +from compas_fab.robots import FrameWaypoints, PointAxisWaypoints, TargetMode + +def test_frame_waypoints_list_behavior(): + fw = FrameWaypoints([Frame.worldXY()], TargetMode.ROBOT) + assert len(fw) == 1 + fw.append(Frame.worldZX()) + assert len(fw) == 2 + + # Test iteration + frames = [f for f in fw] + assert len(frames) == 2 + assert frames[0] == Frame.worldXY() + assert frames[1] == Frame.worldZX() + +def test_point_axis_waypoints_list_behavior(): + pw = PointAxisWaypoints([(Point(0,0,0), Vector(1,0,0))], TargetMode.ROBOT) + assert len(pw) == 1 + pw.append((Point(1,1,1), Vector(0,1,0))) + assert len(pw) == 2 From e6a59a06a900611a9814f7fc3b7e7e2d1a961b48 Mon Sep 17 00:00:00 2001 From: ChickenisLegit Date: Sun, 9 Aug 2026 17:34:46 +0530 Subject: [PATCH 3/4] fix: get_link_names for single link groups --- src/compas_fab/robots/robot_cell.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/compas_fab/robots/robot_cell.py b/src/compas_fab/robots/robot_cell.py index 353c8fdae..64336274e 100644 --- a/src/compas_fab/robots/robot_cell.py +++ b/src/compas_fab/robots/robot_cell.py @@ -449,6 +449,10 @@ def get_link_names(self, group: Optional[str] = None) -> list[str]: group = group or self.main_group_name base_link_name = self.get_base_link_name(group) end_effector_link_name = self.get_end_effector_link_name(group) + + if base_link_name == end_effector_link_name: + return [base_link_name] + link_names = [] for link in self.robot_model.iter_link_chain(base_link_name, end_effector_link_name): link_names.append(link.name) From 9c699f25703bebc29518c7c705d079f8c76c92cd Mon Sep 17 00:00:00 2001 From: ChickenisLegit Date: Sun, 16 Aug 2026 11:26:54 +0530 Subject: [PATCH 4/4] chore: add changelog entry for get_link_names fix --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bc9cb4ef..538d806e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,10 @@ Requires `compas_robots >= 1.1`. * Made `Waypoints` (`FrameWaypoints` and `PointAxisWaypoints`) behave like a list. * The `Tool From Mesh` Grasshopper component gained a `base_plane` input: where the robot's flange takes hold of the geometry, expressed in the coordinates the mesh was modelled in. Its Z axis points away from the robot, so a tool drawn reaching along world Z needs none, and a tool drawn along another axis is mounted by wiring a plane instead of redrawing the geometry. Backed by the new `base_frame` argument of `compas_robots.ToolModel`; nothing is baked into the mesh, so the plane can be re-wired at any time. The component also surfaces a remark when the TCP does not sit roughly on the tool's +Z, since that means the tool will point sideways once attached — the direction from the mount to the TCP is only a hint (it says nothing about roll), so it is reported rather than applied. +### Fixed + +* Fix `get_link_names` for groups with a single link. + ### Changed * The tools in `ToolLibrary` now mount along the +Z axis of their base frame instead of +X. Every planning group in `RobotCellLibrary` ends at a link whose +Z points away from the arm (`tool0` for the industrial robots, `panda_hand_tcp` for the Panda), so with this the same tool attaches to any of them with an identity attachment frame — previously each cell carried a rotation to bridge the two conventions, and a tool authored for one robot did not necessarily fit another. Their TCF states the tool's working direction with its own Z axis too, so a `TargetMode.TOOL` target aligns the tool along the target's Z — previously the TCF's X axis ran along the tool, which put every tool-mode target 90 degrees out. The tools are still modelled along +X internally and re-framed on the way out via `ToolModel.reframe_base`. The beams held by the gripper cells are authored in TCF coordinates and were re-authored to match, so they stay put. Poses are unchanged: the attached tools and workpieces of every cell land exactly where they did, only the tool's base frame is now the end effector link's frame rather than a rotated version of it. Requires the `reframe_base` support of `compas_robots >= 1.1`.