From ffa8327770d9c9dfc0abe44506797c92563a6239 Mon Sep 17 00:00:00 2001 From: Umut CAN Date: Tue, 22 Sep 2026 19:51:00 +0300 Subject: [PATCH] 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) Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd --- .../2026-09-22-v1.6-mech-implementation.md | 164 ++++++++++++++---- 1 file changed, 128 insertions(+), 36 deletions(-) diff --git a/docs/superpowers/plans/2026-09-22-v1.6-mech-implementation.md b/docs/superpowers/plans/2026-09-22-v1.6-mech-implementation.md index 8a883df..d3cdaa3 100644 --- a/docs/superpowers/plans/2026-09-22-v1.6-mech-implementation.md +++ b/docs/superpowers/plans/2026-09-22-v1.6-mech-implementation.md @@ -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, )