Commit Graph
4 Commits
Author SHA1 Message Date
Umut CANandClaude Opus 5 de8dc8f813 fix(task-13): read revision/balloon state from the space it is drawn on, and never at 200
Three findings, one family: a guard that reads the WRONG SET silently stops
guarding and the drawing gets a duplicate or a wrong number with no refusal.

1. Layout-scoped state was read from the wrong space. add_revision called
   read_revisions and add_balloon called read_balloons BEFORE enter_layout;
   both read entity_list, which follows the current space, so the guard
   inspected Model while the rows and balloons lived on the layout -- and the
   layout is the sheet workflow (sheet_frame and titleblock_apply both take
   one). Measured headlessly: two revision_add(rev="B", layout="SHEET1") calls
   both returned created=True, handles 46 and 5A, both bbox
   {min [230,273], max [410,287]} -- a second heading band and a second B row
   drawn exactly on top of the first, twelve TEXTs on the tab instead of six.
   Both reads now happen inside the entered layout, and the whole body sits in
   the try/ so the caller's tab comes back on the refusal path too.

2. bom_extract truncated the parts list at 200 INSERTs and printed a wrong QTY
   into a real TABLE. extract_records passed its own limit=200 default straight
   to entity_list, the tool exposed no limit, and nothing in the payload said
   the list had been cut: 250 bolts reported [("ISO 4014 - M12x60", 200)].
   all_entities now pages entity_list (offset loop, PAGE_SIZE 1000, plus a
   no-new-handles guard so a backend that ignored offset cannot spin) and
   extract_records(limit=None) reads the whole drawing. bom_extract exposes
   limit (0 = no cap) and reports truncated measured, by asking for one record
   past the cap.

3. The balloon clash guard did not fire in two routine cases. (a) targets is
   optional in the tool, so the clash test compared () != () -- False -- and
   two untargeted balloons carried item 1 with no refusal. The renumber arm
   above has already returned for the balloon that stands for these targets, so
   anything still carrying the number is a clash, whatever its targets; the
   message names the handle and says "no targets" when it has none. (b)
   read_balloons and read_revisions took entity_list's 200-entity default, so a
   drawing whose first 200 CIRCLEs are a hole pattern read back no balloons at
   all and every guard passed blind -- and index = len(existing) restarted at
   0, stacking revision bands. Both readers now go through all_entities.

Evidence, headless (ezdxf), each finding's own repro before -> after:
  1  created True/True, handles 46+5A, 12 TEXTs -> created True/False, one
     handle 46, 6 TEXTs; renumbered False/False -> False/True, same handle 33
  2  records 200, rows [(..., 200)] -> records 250, rows [(..., 250)]
  3a two balloons handles 2F+35, no refusal -> refused, naming balloon 2F
  3b read_balloons() == () with 206 circles -> the balloon past the 200th
     circle is found, the rerun renumbers, the item-1/BBB clash is refused
  3c 200 notes + rev A twice + rev B -> created True/False/True, row 1 then
     row 2 (was: every band at row 1, on top of the last)

Six regression tests added: one per finding plus the layout-scoped twin of
each, which is what would have caught all three.

The live engine was UNAVAILABLE for this round:
win32com.client.GetActiveObject("AutoCAD.Application") did not return within
90 s against the running acad.exe (PID 40068), and the ComBackend probe timed
out on its first call at both COM_CALL_TIMEOUT=60 and 180. No second AutoCAD
was launched and no document was opened. No new ActiveX call is introduced by
this fix -- it only reorders existing calls and pages entity_list(offset=),
which both engines already implement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-22 22:39:10 +03:00
Umut CANandClaude Opus 5 499b642fbd feat(sheet): revisions, the ISO 7573 parts list and ISO 6433 balloons
SECTION 24 grows to six tools: revision_add, bom_extract, bom_table,
balloon_add.

bom_extract reads. It walks the INSERTs through the generic members already in
the contract -- entity_list, block_get_attributes, entity_get_xdata -- and
returns parts-list rows from the ACADMCP_MECH std_part payload, falling back to
the block's own ATTRIBs and reporting which of the two each row came from. A
block with neither is skipped rather than guessed at, and the tool never writes
to the drawing.

bom_table draws the rows as a real TABLE, 180 mm wide (ISO 7573: the item list
is as wide as the title block), growing upwards from directly above it with the
heading row against the block and item 1 the row above -- which is what ISO 7573
asks for on a drawing. A TABLE is drawn downwards from its insertion point, so
the upward list is inserted a table height above its anchor with the heading as
the last row; one table_layout feeds both the real TABLE and the pure
table_prims, so the two renderings cannot disagree.

balloon_add draws the ISO 6433 reference -- numeral, leader, dot on the item --
and writes the balloon payload naming its targets. A rerun over the same
targets renumbers that balloon instead of drawing a second one, and an item
number already used for different targets is refused by name with the other
balloon's handle.

revision_add appends a row to the revision block, is idempotent per revision
code (a repeat draws nothing at all), and optionally clouds the changed regions
and tags them with the triangle. Clouds are refused up front, carrying
capability "revcloud", on an engine that has no revcloud member -- the COM
backend -- rather than leaving half a revision behind.

Declared: ISO 7200 does not standardise the revision block's geometry and
ISO 7573 does not fix the item list's column widths or row height, so both
reuse this repository's 180 mm / 7 mm ruling and say so in their SOURCE
docstrings. ISO 6433's "numeral at least twice the dimension character height"
is met by the numeral being 1.4x the balloon radius: the 4 mm default clears
2.5 mm dimension text, and balloon_add's docstring tells a sheet dimensioned at
3.5 mm to pass radius=5.0.

Three corrections to what the plan wrote, each measured rather than assumed:

* extract_records read `block_get_attributes(...)["attributes"]`. Both engines
  return the flat {tag: value} mapping itself (ezdxf_backend.py:6687 builds it
  from `ent.attribs`, com_backend.py:5703 from `ref.GetAttributes()` as
  {TagString: TextString}), so every attribute fallback would have been
  silently empty -- measured headlessly, where a PLATE insert carrying only a
  DESIGNATION ATTRIB extracted to nothing.
* draw_bom_table now reports `entity_type` / `representation` /
  `child_handles`. `entity_create_table` returns EntityInfo(type="TABLE") whose
  handle is the first CHILD of a composite on the headless engine, so reading
  that handle back gives a rule LINE, not an ACAD_TABLE -- the contract
  tests/test_table_leader.py already pins. A caller that means to edit the
  table afterwards has to be told which of the two it got.
* add_revision reports one `tags` entry per tag asked for ({at, handles}), not
  one per entity: a tag is two primitives, so a flat handle list claimed twice
  as many tags as the caller requested.

Discovery: `bom_extract` / `bom_table` / `balloon_add` each entered the index
heavy with "parts"/"list" tokens and took top-1 for the bare query "parts list"
away from `entity_create_table` (a BM25 tie broken alphabetically). Narrowed on
my side only -- the reader speaks of the bill of materials, the drawer names a
table, the balloon names an item -- never by touching the golden case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-22 20:48:50 +03:00
Umut CANandClaude Opus 5 e57ae8b3ac fix(task-12): restore the caller's tab from the layout listing; source the projection symbol
enter_layout read the caller's space from `backend._current_space`, which only
EzdxfBackend has. On ComBackend the getattr fell through to "Model", so
`sheet_frame` / `titleblock_apply` with a `layout=` dropped a live caller onto
the Model tab instead of putting them back -- the opposite of what the
docstring and both tool docs promise, and a dual-engine parity break in new
shared infrastructure. Measured on the live seat (AutoCAD 2026, scratch
document from drawing_new(), closed with discard):

    scratch document: Drawing24.dwg
    layouts: ['Layout1', 'Layout2', 'Model']
    B) current BEFORE: Layout2
    B) the value the OLD code read (backend._current_space):
       <<absent -> getattr default 'Model'>>

`layout_list()` already reported the right value two lines earlier on both
engines (ezdxf from $TILEMODE / the active layout, COM from
`doc.ActiveLayout.Name` -- read live as "Layout2" above), so `previous` now
comes from `listing["current"]`.

The projection-angle symbol was justified by an in-code derivation that cannot
decide the question, and the test restated the implementation. The derivation
was also applied inconsistently: the third-angle symbol mirrored the cone as
well as the end view, leaving the trapezoid's short side pointing AWAY from the
circles -- which is the first-angle arrangement. Transcribed instead from two
independent reproductions of the ISO 5456-2 figure, both downloaded and
measured (ISO's own figure is paywalled and was NOT read; the module says so):

  * FreeCAD's ISO 5457 sheet template A3_Landscape_ISO5457_advanced.svg,
    `first_angle_trapezoid` = "m 389,222 -10,2.5 v 5 l 10,2.5 z" -> 5-unit side
    at x=379, 10-unit side at x=389; circles r2.5/r5 at cx=396.
  * Wikimedia Commons "Convention placement vues dessin technique.svg", both
    symbols side by side, labelled FR and US: FR 35.856 @ x=13.343 / 66.135 @
    x=81.869, circles x=148.005; US 35.856 @ x=380.275 / 66.135 @ x=448.801,
    circles x=314.140.

Both draw the trapezoid the same way -- short side left -- and move only the
end view: first angle puts it right of the cone, third angle left. Equivalently
the short side points away from the circles in first angle and towards them in
third, which is how every source states it in words. `third` now follows.

Tests: the reference figures are transcribed into the test as data, and the
symbol is asserted to land its three landmarks in the order each figure does,
so the test cannot pass by agreeing with the code. The layout restore is pinned
through a proxy that hides `_current_space` from the real backend rather than a
fake that owns members ComBackend does not have. Both new tests fail on the
previous code (measured: "assert 16.0 < 8.0" for the mirrored cone; "- SheetB
+ Model" for the tab).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-22 19:49:28 +03:00
Umut CANandClaude Opus 5 56e6ff7206 feat(sheet): ISO 5457 frames and ISO 7200 title blocks for A4-A0
SECTION 24 opens with sheet_frame and titleblock_apply.

engineering/sheet/frames.py transcribes ISO 216's trimmed sizes, ISO 5457's
20 mm filing margin and 10 mm edges, the grid reference system (an even number
of divisions per side, that side of the frame over 50 mm; letters from the top,
numbers from the left), the centring marks (trimmed edge to 5 mm inside the
frame) and the 10 x 5 mm corner trimming marks. Every one of those numbers is
pinned by a test against the clause it comes from, because a wrong margin is
visible on every drawing this server makes.

engineering/sheet/titleblock.py draws the ISO 7200 block -- 180 mm wide, which
is exactly the drawing-space width of a portrait A4, which is why one block
fits every size -- and carries the ISO 128-30 projection-angle symbol. Which
symbol is which is derived rather than remembered: first-angle projection puts
the view seen from the left on the right, and the view of the cone from its
small end is the small circle inside the large one, so first angle draws the
cone left and the circles right.

The old A3 module folds in. apply_iso_a3_titleblock now forwards to
apply_titleblock(size="A3", projection=None, zones=False, marks=False), which is
the geometry it always drew, and tests/test_engineering_titleblock.py passes
unchanged -- that is the regression gate for the fold.

No new ActiveX member is called: both tools reach the drawing only through
contract members the A3 title block has used on both engines since v1.0
(entity_create_polyline / _line / _circle / _text, layout_list,
layout_set_current), so there was nothing to probe live.

Declared, not glossed: the zone divisions drawn here are equal, so ISO 5457's
allowance for shorter corner fields is not implemented; the division counts,
which are what a zone reference quotes, are the standard's. ISO 7200's full
optional-field list is not transcribed, only the fields this block has a cell
for. Coverage is A4-A0; an unknown size is refused by name with the five it has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-22 19:03:14 +03:00