fix(task-12): keep the plan's task-12 body in step with the shipped fix

The plan embeds Task 12's module and tests verbatim, so it still carried the
`_current_space` read and the derivation that put the third-angle cone's short
side away from the circles. Both replaced with what shipped, so a re-run of the
task cannot transcribe the defects back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
Umut CAN
2026-09-22 19:51:00 +03:00
co-authored by Claude Opus 5
parent e57ae8b3ac
commit ffa8327770
@@ -16288,7 +16288,12 @@ async def enter_layout(backend: AutoCADBackend, layout: str | None, *, caller: s
f"{caller}: layout {layout!r} does not exist "
f"(have: {', '.join(sorted(names))}). Create it with layout_create first."
)
previous = getattr(backend, "_current_space", "Model")
# The caller's tab comes from the listing that was just read -- both engines
# report it there (ezdxf from $TILEMODE / the active layout, COM from
# ``doc.ActiveLayout.Name``). A private attribute would not: only
# EzdxfBackend has ``_current_space``, so reading it left a live caller
# standing on Model after every border.
previous = str(listing.get("current") or "Model")
switched = await backend.layout_set_current(layout)
if not switched.get("ok", True):
raise RuntimeError(f"{caller}: {switched.get('error')}")
@@ -16502,29 +16507,87 @@ def test_projection_none_draws_no_symbol_and_first_draws_one():
assert len(first) == len(plain) + 4
def test_first_angle_puts_the_circles_to_the_right_of_the_cone():
#: The ISO 5456-2 / ISO 128-30 projection symbol, transcribed from two
#: independent reproductions of the standard's figure (ISO's own is paywalled
#: and was not read). Each entry is that source's OWN coordinates, measured off
#: the file, and the landmark order left-to-right is what the symbol means.
#:
#: 1. FreeCAD's ISO 5457 sheet template, first-angle symbol --
#: src/Mod/TechDraw/Templates/ISO/A3_Landscape_ISO5457_advanced.svg,
#: ids `first_angle_trapezoid`, `first_angle_inner circle`,
#: `first_angle_outer circle`.
#: 2. Wikimedia Commons `Convention placement vues dessin technique.svg`, which
#: draws both symbols side by side, labelled FR (first) and US (third).
#:
#: A test that read these numbers out of `projection_symbol_prims` instead
#: would pass whichever way round the symbol was drawn, which is exactly how
#: the mirrored third-angle cone shipped in the first place.
REFERENCE_FIGURES = {
"first": (
# source, short-side x, long-side x, end-view (circles) x
("FreeCAD A3_Landscape_ISO5457_advanced.svg", 379.0, 389.0, 396.0),
("Commons Convention placement vues dessin technique.svg (FR)", 13.343, 81.869, 148.005),
),
"third": (
("Commons Convention placement vues dessin technique.svg (US)", 380.275, 448.801, 314.140),
),
}
#: The three landmarks, in the order `_landmarks` and REFERENCE_FIGURES list them.
LANDMARKS = ("short", "long", "circles")
def _landmarks(angle: str) -> tuple[float, float, float]:
"""(short-side x, long-side x, end-view x) of the symbol this module draws."""
prims = projection_symbol_prims((0.0, 0.0), angle)
cone = next(p for p in prims if isinstance(p, Poly))
circles = [p for p in prims if isinstance(p, Circle)]
left_x = min(x for x, _ in cone.points)
right_x = max(x for x, _ in cone.points)
left_h = max(y for x, y in cone.points if x == left_x) * 2
right_h = max(y for x, y in cone.points if x == right_x) * 2
short_x, long_x = (left_x, right_x) if left_h < right_h else (right_x, left_x)
assert len({c.center[0] for c in circles}) == 1
return short_x, long_x, circles[0].center[0]
@pytest.mark.parametrize("angle", ["first", "third"])
def test_the_symbol_lands_the_landmarks_in_the_order_the_reference_figures_do(angle):
"""The meaning of the symbol is the left-to-right order of three landmarks:
the cone's short side, its long side and the end view. Assert that order
against the measured figures, not against what the module computes."""
mine = _landmarks(angle)
for source, *reference in REFERENCE_FIGURES[angle]:
expected = [name for _x, name in sorted(zip(reference, LANDMARKS, strict=True))]
got = [name for _x, name in sorted(zip(mine, LANDMARKS, strict=True))]
assert got == expected, f"{angle}-angle symbol disagrees with {source}"
def test_the_short_side_points_away_from_the_circles_in_first_and_towards_in_third():
"""The mirror-invariant half of the rule, and the one every source states
in words: Wikipedia's Multiview orthographic projection, Symbol --
"the first-angle symbol shows the trapezoid with its shortest side away
from the circles", "the third-angle symbol shows the trapezoid with its
shortest side towards the circles"."""
short_x, long_x, circles_x = _landmarks("first")
assert abs(short_x - circles_x) > abs(long_x - circles_x)
short_x, long_x, circles_x = _landmarks("third")
assert abs(short_x - circles_x) < abs(long_x - circles_x)
def test_the_symbol_keeps_the_reference_figures_proportions_and_its_axis():
"""Both reference figures draw the end view as two circles of ratio 1:2
(FreeCAD r 2.5 / r 5) on a centre line; the absolute size is this
module's."""
first = projection_symbol_prims((0.0, 0.0), "first")
third = projection_symbol_prims((0.0, 0.0), "third")
cone_first = next(p for p in first if isinstance(p, Poly))
circles_first = [p for p in first if isinstance(p, Circle)]
assert max(x for x, _ in cone_first.points) < min(c.center[0] for c in circles_first)
cone_third = next(p for p in third if isinstance(p, Poly))
circles_third = [p for p in third if isinstance(p, Circle)]
assert min(x for x, _ in cone_third.points) > max(c.center[0] for c in circles_third)
assert {round(c.radius, 6) for c in circles_first} == {4.0, 2.0}
circles = [p for p in first if isinstance(p, Circle)]
assert {round(c.radius, 6) for c in circles} == {4.0, 2.0}
assert any(isinstance(p, Line) and p.role == "center" for p in first)
def test_the_cone_s_small_end_faces_the_circles_side_in_first_angle():
"""The derivation only holds if the view drawn as circles really is the
view from the cone's small end."""
cone = next(p for p in projection_symbol_prims((0.0, 0.0), "first") if isinstance(p, Poly))
left_x = min(x for x, _ in cone.points)
right_x = max(x for x, _ in cone.points)
left_height = max(y for x, y in cone.points if x == left_x) * 2
right_height = max(y for x, y in cone.points if x == right_x) * 2
assert left_height == pytest.approx(4.0, abs=EPS)
assert right_height == pytest.approx(8.0, abs=EPS)
def test_the_two_symbols_are_not_the_same_picture():
assert _landmarks("first") != _landmarks("third")
def test_an_unknown_projection_angle_is_refused():
@@ -16757,34 +16820,63 @@ def titleblock_origin(size: str, *, orientation: str = "landscape") -> Pt:
def projection_symbol_prims(at: Pt, angle: str = "first") -> tuple[Prim, ...]:
"""The ISO 128-30 projection-angle symbol, centred on `at`.
"""The ISO 5456-2 / ISO 128-30 projection-angle symbol, centred on `at`.
Arrangement, and where it comes from. The symbol is a truncated cone with a
horizontal axis and its small end facing left. The first- and third-angle
symbols are mirror images about a vertical axis, and which is which follows
from the projection rule itself: first-angle projection puts the view seen
from the left ON THE RIGHT, and the view of this cone seen from the left is
the small circle inside the large one. So FIRST angle draws the cone on the
left and the concentric circles on the right; THIRD angle is its mirror.
ARRANGEMENT -- transcribed, not derived. The symbol is a truncated cone
drawn in two views: the cone in elevation (a trapezoid) and the end view
(two concentric circles). Which side the end view sits on is what makes the
symbol mean *first* or *third* angle, and it is a convention that has to be
read off the standard's figure: the projection rule alone cannot pick it,
because the same picture is a legal drawing of the mirrored object seen
from the other side.
ISO 5456-2's own figure is behind ISO's paywall and was not read. Two
independent reproductions of it were downloaded and measured instead, and
they agree on every landmark used here:
* FreeCAD's ISO 5457 sheet template, first-angle symbol
(`src/Mod/TechDraw/Templates/ISO/A3_Landscape_ISO5457_advanced.svg`,
ids `first_angle_trapezoid` / `first_angle_*_circle`): the trapezoid is
`m 389,222 -10,2.5 v 5 l 10,2.5 z` -- a 5-unit side at x = 379 and a
10-unit side at x = 389 -- and the circles (r 2.5 and r 5) are at
cx = 396. Short side LEFT; end view to the RIGHT of the cone.
* Wikimedia Commons `Convention placement vues dessin technique.svg`, which
draws both symbols side by side, labelled FR and US. FR (first angle):
trapezoid 35.856 units tall at x = 13.343 and 66.135 at x = 81.869,
circles at x = 148.005. US (third angle): trapezoid 35.856 at
x = 380.275 and 66.135 at x = 448.801, circles at x = 314.140.
So, in both symbols alike, the trapezoid's SHORT side is on the left; only
the end view changes side:
first angle cone, then the circles to its RIGHT
third angle the circles, then the cone to their RIGHT
which is the same thing as the rule the standard is usually quoted by --
the short side points AWAY from the circles in first angle and TOWARDS
them in third. `tests/test_sheet_titleblock.py` pins the ordering against
the measured figures above, so this cannot be "fixed" back by eye.
The proportions (large circle diameter 8, small 4, cone 8 mm long) are this
module's; ISO 5456-2's proportion table is not transcribed. The arrangement
is the part that can be *wrong* on a drawing, and it is derived above.
module's; ISO 5456-2's proportion table is not transcribed. They keep the
1:2 small-to-large ratio both reference figures' circles have.
"""
if angle not in PROJECTION_ANGLES:
raise ValueError(f"projection angle must be one of {PROJECTION_ANGLES}, got {angle!r}")
cx, cy = float(at[0]), float(at[1])
half = (PROJECTION_CONE_LENGTH + PROJECTION_GAP + 2 * PROJECTION_R) / 2.0
# The end view sits right of the cone in first angle, left of it in third.
sign = 1.0 if angle == "first" else -1.0
cone_far = -half * sign
cone_near = cone_far + sign * PROJECTION_CONE_LENGTH
circles_x = (half - PROJECTION_R) * sign
# The trapezoid itself is the same picture in both symbols: short side left.
cone_left = -half if angle == "first" else half - PROJECTION_CONE_LENGTH
cone_right = cone_left + PROJECTION_CONE_LENGTH
cone = Poly(
points=(
(cx + cone_far, cy - PROJECTION_SMALL_R),
(cx + cone_near, cy - PROJECTION_R),
(cx + cone_near, cy + PROJECTION_R),
(cx + cone_far, cy + PROJECTION_SMALL_R),
(cx + cone_left, cy - PROJECTION_SMALL_R),
(cx + cone_right, cy - PROJECTION_R),
(cx + cone_right, cy + PROJECTION_R),
(cx + cone_left, cy + PROJECTION_SMALL_R),
),
closed=True,
)