From 73b1d51957b75526ab31c49a1fa65476ead6cb36 Mon Sep 17 00:00:00 2001 From: jepson2k <55201008+Jepson2k@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:00:46 -0400 Subject: [PATCH] Dry-run snap carries the pending blend chain _snap_to_angles (teleport, un-referenced home, home(calibrate=True)) flushed the planner's blend buffer and discarded what it produced, so a blended move issued right before the snap vanished from the preview along with any collision refusal it would have raised live. The flushed results now lead the merged result, as par6's dry-run client already does. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_0158QrPR5eVhd31AvFpxJKr6 --- parol6/client/dry_run_client.py | 14 +++++++++++--- tests/unit/test_dry_run_script_compat.py | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/parol6/client/dry_run_client.py b/parol6/client/dry_run_client.py index a60c6fb..8b04a49 100644 --- a/parol6/client/dry_run_client.py +++ b/parol6/client/dry_run_client.py @@ -238,14 +238,22 @@ def _snap_to_angles(self, angles_deg: list[float]) -> DryRunResult: """Snap to angles instantly (no trajectory) — used by Home and Teleport. Both establish position references, so subsequent planned moves pass - the homed gate.""" - self._planner.flush() + the homed gate. Blended moves still buffered in the planner are + planned first and lead the returned result, so their paths — and any + refusal — reach the caller exactly as the live controller would run + them before the snap.""" + pending = [ + r + for seg in self._planner.flush() + if (r := self._segment_to_result(seg)) is not None + ] deg = np.asarray(angles_deg, dtype=np.float64) deg_to_steps(deg, self._state.Position_in) self._planner.state.Position_in[:] = self._state.Position_in self._planner.state.Homed_in.fill(1) rad = np.radians(deg).reshape(1, -1) - return _build_result(rad, duration=0.0) + snap = _build_result(rad, duration=0.0) + return self._merge_results([*pending, snap]) if pending else snap def _dispatch(self, params: Any) -> DryRunResult | None: """Route a command struct through the trajectory planner.""" diff --git a/tests/unit/test_dry_run_script_compat.py b/tests/unit/test_dry_run_script_compat.py index 0a46714..b6e89e8 100644 --- a/tests/unit/test_dry_run_script_compat.py +++ b/tests/unit/test_dry_run_script_compat.py @@ -9,6 +9,7 @@ from the real client. """ +import numpy as np import pytest from parol6.client.dry_run_client import DryRunRobotClient @@ -161,3 +162,17 @@ def test_referenced_home_previews_as_return_move(self): result = client.home() assert result is not None and result.error is None assert result.duration == 0.0 + + def test_snap_carries_the_pending_blend_chain(self): + """A blended move still buffered when the script homes with calibrate + (or teleports) is planned and leads the returned result — the live + controller runs it before the snap, so the preview must show it.""" + client = DryRunRobotClient(initial_joints_deg=HOME, initial_homed=True) + assert client.move_j(ANGLES_A, speed=0.5, r=10) is None # buffered + + result = client.home(calibrate=True) + assert result is not None and result.error is None + assert result.duration > 0.0 + assert len(result.joint_trajectory_rad) > 1 + assert np.allclose(np.degrees(result.end_joints_rad), HOME, atol=0.5) + assert client.flush() == []