fix(task-23): user_select reports the picked point in WCS — GetEntity answers in the current UCS, so translate acUCS→acWorld before returning
Measured on AutoCAD 2026: under a UCS at (100,50) rotated 90 deg, a pick on a circle at WCS (105,70) came back from IAcadUtility.GetEntity as (20,-5,0), while Utility.GetPoint already answered in WCS. A model doing ucs_set -> user_select -> entity_create_* at `picked` would place geometry at a silently wrong position. The point goes through Utility.TranslateCoordinates(acUCS, acWorld) packed as a VT_ARRAY|VT_R8 VARIANT (the plain tuple GetEntity hands back is refused as "Invalid argument Point"). The fake Utility now models the UCS frame and the VARIANT requirement; a new test pins the measured figures and checks GetPoint stays untranslated. Docstrings state WCS. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
co-authored by
Claude Opus 5
parent
b83a74e969
commit
2052915f34
+19
-1
@@ -108,6 +108,11 @@ def _com_teardown():
|
||||
pass
|
||||
|
||||
|
||||
# AcCoordinateSystem, as Utility.TranslateCoordinates takes it (From / To).
|
||||
_AC_WORLD = 0
|
||||
_AC_UCS = 1
|
||||
|
||||
|
||||
def _apoint(x: float, y: float, z: float = 0.0):
|
||||
"""Create a VARIANT double-array point for AutoCAD COM."""
|
||||
return win32com.client.VARIANT(
|
||||
@@ -2081,11 +2086,24 @@ class ComBackend(AutoCADBackend):
|
||||
if reason is None:
|
||||
raise
|
||||
return {"cancelled": True, "reason": reason, "handles": [], "backend": "com"}
|
||||
# MEASURED (AutoCAD 2026): GetEntity's PickedPoint is in the
|
||||
# *current UCS* — under a UCS at (100,50) rotated 90 deg, a pick
|
||||
# on a circle at WCS (105,70) came back as (20,-5,0) — whereas
|
||||
# Utility.GetPoint already answers in WCS. Every coordinate out
|
||||
# of a tool is WCS, so translate acUCS → acWorld. The point must
|
||||
# be a VT_ARRAY|VT_R8 VARIANT: the tuple GetEntity hands back is
|
||||
# refused as "Invalid argument Point".
|
||||
world = doc.Utility.TranslateCoordinates(
|
||||
_apoint(picked[0], picked[1], picked[2] if len(picked) > 2 else 0.0),
|
||||
_AC_UCS,
|
||||
_AC_WORLD,
|
||||
False,
|
||||
)
|
||||
return {
|
||||
"cancelled": False,
|
||||
"mode": "single",
|
||||
"handles": [str(obj.Handle)],
|
||||
"picked": [float(picked[0]), float(picked[1])],
|
||||
"picked": [float(world[0]), float(world[1])],
|
||||
"backend": "com",
|
||||
}
|
||||
doc.Utility.Prompt(f"\n{text}\n")
|
||||
|
||||
@@ -148,7 +148,7 @@ class EnvironmentContract(ABC):
|
||||
reason="prompting the operator needs a live AutoCAD session",
|
||||
)
|
||||
async def user_select(self, prompt: str, mode: str = "single") -> dict:
|
||||
"""→ ``{"cancelled", "handles": [...]}``."""
|
||||
"""→ ``{"cancelled", "handles": [...]}``; ``mode="single"`` adds ``"picked": [x, y]`` in WCS."""
|
||||
...
|
||||
|
||||
@capability( # noqa: B027 — @capability supplies the body
|
||||
|
||||
@@ -7240,7 +7240,9 @@ async def user_select(
|
||||
ctx: Context = None,
|
||||
) -> dict:
|
||||
"""Returns `handles` the operator picked — one with `mode="single"` (plus
|
||||
the `picked` point), any number with `mode="multiple"` (Enter with nothing
|
||||
the `picked` point `[x, y]` in **WCS** — AutoCAD reports it in the current
|
||||
UCS and the tool translates, so a UCS made current by `ucs_set` never
|
||||
shifts it), any number with `mode="multiple"` (Enter with nothing
|
||||
selected is `count: 0`, not a cancel). ESC → `cancelled: true`; a wait
|
||||
longer than `COM_CALL_TIMEOUT` → `timed_out: true`. Refusals: a mode
|
||||
outside the two, an empty prompt; headless, `capability:
|
||||
|
||||
@@ -157,9 +157,21 @@ def _com_error(description: str):
|
||||
|
||||
|
||||
class _FakeUtility:
|
||||
"""IAcadUtility as measured on AutoCAD 2026.
|
||||
|
||||
``GetPoint`` answers in WCS; ``GetEntity``'s PickedPoint is in the
|
||||
*current UCS* (``ucs_origin`` / ``ucs_xdir`` / ``ucs_ydir``, world by
|
||||
default); ``TranslateCoordinates`` only accepts a VT_ARRAY|VT_R8 VARIANT —
|
||||
the plain tuple GetEntity returns is refused as "Invalid argument Point".
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.calls: list[tuple[str, tuple]] = []
|
||||
self.point = (12.5, 7.25, 0.0)
|
||||
self.picked_ucs = (3.0, 4.0, 0.0)
|
||||
self.ucs_origin = (0.0, 0.0, 0.0)
|
||||
self.ucs_xdir = (1.0, 0.0, 0.0)
|
||||
self.ucs_ydir = (0.0, 1.0, 0.0)
|
||||
self.raise_on_pick = None
|
||||
self.entity = types.SimpleNamespace(Handle="1F3")
|
||||
|
||||
@@ -173,7 +185,34 @@ class _FakeUtility:
|
||||
self.calls.append(("GetEntity", (prompt,)))
|
||||
if self.raise_on_pick is not None:
|
||||
raise self.raise_on_pick
|
||||
return self.entity, (3.0, 4.0, 0.0)
|
||||
return self.entity, self.picked_ucs
|
||||
|
||||
def TranslateCoordinates(self, point, from_cs, to_cs, displacement):
|
||||
import pythoncom
|
||||
from win32com.client import VARIANT
|
||||
|
||||
if not isinstance(point, VARIANT) or point.varianttype != (
|
||||
pythoncom.VT_ARRAY | pythoncom.VT_R8
|
||||
):
|
||||
raise _com_error("Invalid argument Point in TranslateCoordinates")
|
||||
self.calls.append(
|
||||
("TranslateCoordinates", (tuple(point.value), from_cs, to_cs, displacement))
|
||||
)
|
||||
assert (from_cs, to_cs, displacement) == (1, 0, False) # acUCS → acWorld, a point
|
||||
u, v, w = point.value
|
||||
ox, oy, oz = self.ucs_origin
|
||||
xx, xy, xz = self.ucs_xdir
|
||||
yx, yy, yz = self.ucs_ydir
|
||||
zx, zy, zz = (
|
||||
xy * yz - xz * yy,
|
||||
xz * yx - xx * yz,
|
||||
xx * yy - xy * yx,
|
||||
)
|
||||
return (
|
||||
ox + u * xx + v * yx + w * zx,
|
||||
oy + u * xy + v * yy + w * zy,
|
||||
oz + u * xz + v * yz + w * zz,
|
||||
)
|
||||
|
||||
def Prompt(self, text):
|
||||
self.calls.append(("Prompt", (text,)))
|
||||
@@ -336,11 +375,42 @@ async def test_com_select_single_uses_getentity(com_backend):
|
||||
"picked": [3.0, 4.0],
|
||||
"backend": "com",
|
||||
}
|
||||
assert document.Utility.calls == [("GetEntity", ("\nPick the flange",))]
|
||||
assert document.Utility.calls == [
|
||||
("GetEntity", ("\nPick the flange",)),
|
||||
("TranslateCoordinates", ((3.0, 4.0, 0.0), 1, 0, False)),
|
||||
]
|
||||
document.Utility.raise_on_pick = _com_error("Function cancelled")
|
||||
assert (await backend.user_select("Pick"))["cancelled"] is True
|
||||
|
||||
|
||||
async def test_com_select_single_reports_the_pick_in_wcs_under_a_ucs(com_backend):
|
||||
"""MEASURED (AutoCAD 2026): GetEntity's PickedPoint is in the current UCS.
|
||||
|
||||
UCS origin (100,50,0) rotated 90 deg (X dir (0,1,0)); the operator picked
|
||||
a circle at WCS (105,70): GetEntity reported (20,-5,0) and
|
||||
TranslateCoordinates(acUCS → acWorld) gave (105,70,0). GetPoint already
|
||||
answers in WCS, so ``user_pick_point`` and ``user_select`` must agree.
|
||||
"""
|
||||
backend, app, document = com_backend
|
||||
utility = document.Utility
|
||||
utility.ucs_origin = (100.0, 50.0, 0.0)
|
||||
utility.ucs_xdir = (0.0, 1.0, 0.0)
|
||||
utility.ucs_ydir = (-1.0, 0.0, 0.0)
|
||||
utility.picked_ucs = (20.0, -5.0, 0.0)
|
||||
result = await backend.user_select("Pick the circle")
|
||||
assert result["picked"] == [105.0, 70.0]
|
||||
assert result["handles"] == ["1F3"] and result["cancelled"] is False
|
||||
# A translated UCS without rotation (origin (100,50)): (15,20) → (115,70).
|
||||
utility.ucs_xdir, utility.ucs_ydir = (1.0, 0.0, 0.0), (0.0, 1.0, 0.0)
|
||||
utility.picked_ucs = (15.0, 20.0, 0.0)
|
||||
assert (await backend.user_select("Pick"))["picked"] == [115.0, 70.0]
|
||||
# GetPoint is not translated — it already answers in WCS.
|
||||
utility.point = (110.0, 70.0, 0.0)
|
||||
point = await backend.user_pick_point("Pick")
|
||||
assert (point["x"], point["y"]) == (110.0, 70.0)
|
||||
assert not any(name == "TranslateCoordinates" for name, _ in utility.calls[-1:])
|
||||
|
||||
|
||||
async def test_com_select_multiple_uses_a_selection_set_and_deletes_it(com_backend):
|
||||
backend, app, document = com_backend
|
||||
result = await backend.user_select("Select the pipes", mode="multiple")
|
||||
|
||||
Reference in New Issue
Block a user