b2f858212f53e67878e8c0c92825932c7877b220
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b2f858212f |
fix(task-4): the end-view occlusion rule, the double-drawn groove, the left view
Three defects in the orthographic view engine, all one missing idea: the engine
had no visibility rule and no single owner for the silhouette.
1. `_end_circles` inverted the hidden-line rule. Walking the bands from the
viewing end it drew a circle only when its radius EXCEEDED everything nearer,
and roled every such circle `hidden` unless it was the first band - so the one
edge that CANNOT be occluded (a step face larger than the material in front of
it) shipped dashed on HIDDEN, and the circle that really is covered was not
drawn at all. `_front_max` is now the rule: an edge of radius r at station x is
continuous when r is larger than every outer radius between x and the observer,
dashed when it is not, and it is always drawn. Bores follow the mirror image -
you see down a bore only while it stays at least as wide. Each distinct radius
is emitted once, because two coincident circles are a duplicate entity.
The same rule now roles a feature's axis-centred end-view circle, whose axial
station comes from `removal_box`; a feature that reports none keeps its own
verdict rather than having one guessed.
Ground truth by ray cast along the axis of the same solid of revolution
(segments 20x25, 40x30, 15x20; observer at +X, probe just inside each edge):
r=10 at x=75 -> first surface x=75 -> VISIBLE (was visible, kept)
r=15 at x=60 -> first surface x=60 -> VISIBLE (was HIDDEN, fixed)
r=12.5 at x=20 -> first surface x=60 -> HIDDEN (was not drawn, fixed)
2. Every axisymmetric groove was drawn twice - once as the notch
`radial_profile` cuts into the silhouette, once by the feature's own
`prims()`, which `_feature_layer` appended unconditionally. Six exactly
coincident LINEs per grooved shaft, which is precisely what
`critique.py::_check_duplicate_entities` matches, so premium rule 7 could
never be met on a part carrying a retaining or O-ring groove. `_profile_owned`
now drops a feature primitive that the body already emitted, and drops a
revolved front view's `visible` silhouette line from any feature that edits
the radial profile: the spec's rule is that grooves cut a notch in the
silhouette, and the notch belongs to the one calculation. Suppression is
"the profile already drew this", not "this feature edits the profile", so a
blind bore keeps the bottom line the profile cannot express, and an internal
thread keeps its ISO 6410 limit line.
A groove straddling a step was worse than duplicated: the feature reads one
surface radius for a span that has two, so it drew a groove floor through
solid material and left the +Y path with four odd-degree nodes. It now has
two - its two ends on the axis.
3. `build_view` validated `side` and put it in ctx, but the prismatic branch
ignored it: `side="left"` returned the right-hand view, with every feature on
the wrong side of the sheet and a symmetric outline rectangle to hide it. An
observer at -X sees screen-right = -Y, so the feature layer is now mirrored
about the outline's mid-Y. The top view is always seen from above and does
not move with `side`; that is now said in the docstring and pinned by a test.
Tests: the two that pinned the inverted rule are rewritten around the ray cast,
and one of them contradicted its own name. Seven tests added, and the prismatic
side-view test's hole moved off the y=25 mirror axis, where it could not tell a
left view from a right one. Nine of them fail against the previous `views.py`
and pass against this one.
Live AutoCAD was unavailable for a SOLPROF cross-check: the running AutoCAD 2026
(pid 40068) reports Responding=False and `GetActiveObject("AutoCAD.Application")`
never returned inside 100 s, so no scratch document was created and none is left
open. No new ActiveX member is called by this change - views.py is pure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
|
||
|
|
48eb057273 |
feat(task-4): the orthographic view engine - silhouette, hidden lines, layout
engineering/mech/views.py projects a part model onto a view plane and returns primitive descriptions, so a test asserts geometry rather than a screenshot. The one calculation everything comes out of for a revolved part is the radial profile: the part cut into bands of constant bore and constant (or linearly tapering) outside, with every feature's radial_edit applied. The front view is that profile walked as a path and mirrored about the axis; the section cut face of the next task is the same profile walked as a closed loop, which is what stops the outline and the hatched area ever disagreeing. Chamfers and fillets do not draw over the silhouette - they substitute their corner in it, so the overshoot the critique focus looks for cannot be produced by construction, and a corner edit that finds no corner is reported rather than dropped. A band's radii are read off the segment the band's MIDPOINT lies in and the taper is evaluated at the band's own two ends. Sampling a hair inside the band to dodge the segment-boundary ambiguity - the way the plan's draft did - writes the taper slope times that inset into every coordinate the silhouette is built from: a 30 -> 10 mm cone over 20 mm then reports 14.99999 for a radius that is exactly 15, and nothing downstream can tell that it is wrong. The end view applies a real hidden-line rule: walking the bands from the viewing end, a diameter larger than everything nearer than it is drawn hidden, a bore behind material is hidden, and the near end's own circles are visible. Looking from the left reverses it. A revolved part's top view is refused, because it repeats the front. Prismatic parts get the outline in true shape with corner fillets and chamfers substituted in, and side/top thickness rectangles - the case where a hand-drawing model reliably forgets the hidden line per through feature, which here comes out of the feature itself. The honesty rule of spec section 6 is enforced in one place: a feature that emits no primitives and edits neither the radial profile nor a corner is listed in the view's omitted tuple with its own reason. Nothing is silently dropped. layout_origin places views in first-angle positions by default (the view from the right goes to the left, the view from above goes below) and flips both for third angle. It takes one additive keyword beyond the pinned signature, child, because placing a view to the left depends on that view's own width; without it the child is assumed the parent's size and every pinned call still works. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
457c53644f |
fix(task-3): keyway end-view arc, the machined face, and the chamfer's segment bound
Three defects in the feature catalogue, all faithfully transcribed from the plan text and all wrong in shipped code. 1. Keyway.prims(view="side") emitted the arc the keyway REMOVES. `Arc` is CCW start -> end, and the emitted (right wall -> left wall) is the ~31 degree cap over the slot opening, not the ~329 degrees of shaft that survives it. Measured live on AutoCAD 2026 (Documents.Add scratch doc, closed in a finally): ModelSpace.AddArc(center, radius, start_rad, end_rad) -- angles in RADIANS, CCW start -> end -- gives ArcLength 8.098 mm for the emitted 74.534 -> 105.466 deg and 86.150 mm for the reversed 105.466 -> 74.534 deg, against a 94.248 mm full circle at r = 15; StartPoint/EndPoint of the emitted arc are exactly the notch walls at x = +/-4. Swapped, so the arc now runs left wall -> right wall the long way round, and the side view's bbox goes from (-4, 11, 4, 15) to (-15, -15, 15, 14.457). 2. Hole and Pocket cut the plate from OPPOSITE faces in the same view frame: Hole ran z = 0 -> depth, Pocket z = thickness -> thickness - depth, and neither takes a face parameter. A plate carrying both showed the pocket milled from one side and the blind hole drilled from the other, silently -- omission() returns None for both, so nothing reported it. The frame comment now names the machined face (v = thickness, the face the front view looks at), and Hole drills from it: the blind floor, the counterbore step and the through bore all hang down from there, as Pocket already did. 3. Chamfer.validate bounded the axial leg against the whole part, never against the segment the chamfer sits on. A shallow lead-in passed validation and was drawn straight through a shoulder at a radius that segment does not have (size 30 at 10 deg on a 15 mm end segment: radial 5.29 < 10 so the radial guard was happy, yet `before` landed at x = 45, two segments away). New helper _anchor_segment names the segment a corner actually cuts into -- at a step, the adjacent one carrying the larger radius -- and the leg is bounded by its length, refused by name with the step it would have crossed. Tests: the keyway test asserted only primitive counts and two x coordinates, so it passed with the arc reversed; it now pins the sweep, the endpoints and the bbox. The hole test exercised only a through hole, which is symmetric; added a blind-hole-and-pocket test that pins both to the same face, and a counterbore test. Added three chamfer tests (two refusals, one that must still be accepted). Each fix was mutation-checked: reverting it fails its test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
ac6c4075ba |
feat(task-3): the feature catalogue and its standards tables
engineering/mech/features.py is the nineteen typed features of spec section 5,
each answering the same four questions - is my placement legal, what do I
contribute to this view, to a cut face, and what dimension do I want - plus the
five structural hooks the view engine needs: removal_box, radial_edit,
corner_edit, outline_edit and omission. Pure arithmetic; a test asserts emitted
primitive tuples, not a screenshot.
Nothing restates a table the repository already owns: Keyway takes its width
and depth from engineering.keyway (DIN 6885-1) and is refused by name outside
that table's bore range; GearTeeth takes its outline from engineering.gear's
involute generator and is refused when its tip diameter disagrees with the
segment it sits on.
standards/threads.py transcribes ISO 261 Table 1 and Table 2 - first-choice
M1.6-M64 plus second-choice M14/M18/M22/M27/M33/M39/M45/M52/M60 - and computes
ISO 724's basic minor and pitch diameters from ISO 68-1's own formula, pinned
against the published values (M20x2.5 -> 17.294 / 18.376). The remaining
second-choice diameters, every third-choice diameter and fine pitches below M8
are not transcribed and are refused by name, never interpolated. The drawing
rules are ISO 6410: minor at 0.8 x major, a thread-length limit line across the
major, a three-quarter circle in the end view, and an internal thread whose
section hatch runs to the major diameter.
standards/{undercuts,rings,centres,orings}.py ship DIN 509, DIN 471/472,
DIN 332-1 and ISO 3601-2 with their SOURCE, their row-key contract and an
**empty** row set. That is deliberate and is the track's table rule taken at its
word: a row that could not be verified against the standard is not shipped,
because a wrong relief depth or groove diameter is a wrong workshop drawing
while a narrow table is merely narrow. Every lookup is refused by name and says
what to transcribe and where; the features accept explicit dimensions so the
geometry is fully exercised, and DIN 332 form R is refused outright rather than
approximated by the straight 60 degree cone of forms A and B. Filling any of
these tables later is a data edit with no code change, and a shape test already
fails any row that lands without a column the drawing code reads.
Three deviations from the plan text, each measured rather than assumed
(the merge agent should keep exactly one copy of the first):
1. standards/__init__.py (Task 1, on main) refused a registration with no rows
("registered no rows"), which would have made the four not-transcribed
tables impossible to register at all - and registering them is what keeps a
later transcription a data edit. register() now refuses a rows argument that
is not a dict and accepts an empty one; two tests in tests/test_mech_standards.py
pin both halves. No existing test asserted the old refusal.
2. ORingGroove's housing kind is carried by `housing`, not `kind`. A dataclass
field named `kind` overwrites the discriminator _Base.to_dict() writes:
measured, Probe(id='og').to_dict() == {'kind': 'radial', 'id': 'og'}, so
feature_from_dict would then refuse its own payload as an unknown feature
kind and no part carrying an O-ring groove could round-trip through XDATA.
A new test pins the round trip.
3. The plan's end-chamfer test asserted after == (75.0, 10.0) - the corner
itself - and before == (72.0, 8.268), a point on neither surface. The real
edit is before (72.0, 10.0) on the cylinder and after (75.0, 8.268) on the
end face, consistent with the 45-degree start chamfer the same file pins.
The test was corrected to the measured geometry; the behaviour is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
|
||
|
|
04d269318a |
feat(task-2): the mechanical part model - RevolvedPart, PrismaticPart, build_part
engineering/mech/part.py is the model the whole track draws from: a revolved stack of segments along +X, or a closed prismatic outline plus a thickness, each carrying a list of typed features. Pure arithmetic - no backend, no async, no DXF - which is what lets the view engine be tested without a drawing. Every refusal fires before anything is drawn and names the offending path the way pid_from_spec does: a non-finite or non-positive number as segments[2].d_outer, a bore that is not smaller than its outside (a bore that changes diameter is two segments), an outline with fewer than three distinct vertices, an outline that crosses itself (engineering.measure.is_self_intersecting - the shoelace cancels crossed lobes rather than failing, so a bowtie would otherwise measure 0.0), an unknown material, an unknown segment key, a duplicate feature id. Two gates are the reason this is a model rather than a dict: a feature whose placement is off the part is refused by its own name and by the range it missed, and two features that would remove the same material are refused with both names and the overlapping box. Neither is clamped. part_to_dict round-trips through build_part, which is what lets mech_part_draw write the model onto the drawing as ACADMCP_MECH XDATA and mech_view_add read it back months later without the caller re-describing the part. Two fixes to the plan's own code and test list, both recorded in deviations: _features_from imported engineering.mech.features unconditionally, so a featureless part - every fixture in this module, and both round-trips - died on ModuleNotFoundError until Task 3 lands; the import now happens only when there is a feature dict to build. And the plan's test file omitted test_a_duplicate_feature_id_is_refused while its own gate deselects it by name, so the duplicate-id refusal the commit message claims went unpinned; the test is written here and Task 3 turns it green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |
||
|
|
2bb8044144 |
feat(mech): shared foundation - primitives, XDATA codec, standards registry, ISO 128-50 materials
engineering/mech/ opens with the four modules every track-B/G branch imports, so the four worktrees can be cut from this commit and none of them has to invent a name the others will conflict with. - primitives.py: the typed descriptions the pure modules emit (Line, Arc, Circle, Poly, HatchArea, Text, DimIntent) with bbox / translate / rotate / scale, and ROLE_LAYER - the one place an ISO 128 line role becomes a layer name, so no module hardcodes a layer. The arc bbox sweeps its quadrants: a 0-180 arc reports y=+r, not its endpoints, which is what keeps the view layout from stacking two views on top of each other. - xdata.py: the ACADMCP_MECH codec in Track A's shape - a 1001 application marker, 1000 chunks of at most 255 ASCII characters, the 16 KB per-entity ceiling enforced before anything is written, and a foreign application, a corrupt chunk stream, a wrong version or an unknown kind refused with ValueError instead of decoded into something plausible. - standards/__init__.py: the table registry. A size outside the coverage is refused against the coverage; a size inside it that is not a listed row is refused against the rows. Nothing is interpolated, an ambiguous variant is never resolved by taking the first row, and a registration whose SOURCE does not name the table it was transcribed from is refused. - standards/materials.py: the section-hatch map for nine materials. ISO 128-50 standardises only the general hatching (narrow continuous lines at 45 degrees) - that is acadiso.pat ANSI31 and what engineering/gear.py already draws for steel. The distinguishing symbols are ASME Y14.2M's section lining as shipped in AutoCAD 2026's acadiso.pat, read off the installed file; cast iron's departure from ASME and the absence of any mechanical symbol for concrete and wood are stated in the rows themselves. Angles are 0.0 because the patterns already carry 45 degrees, and the two architectural patterns' scales are derived from the ANSI31 line spacing and recomputed by the test rather than chosen. No tool is registered here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd |