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/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]. 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