draftwright 0.3.2__tar.gz → 0.3.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {draftwright-0.3.2 → draftwright-0.3.3}/CHANGELOG.md +44 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/PKG-INFO +1 -1
- {draftwright-0.3.2 → draftwright-0.3.3}/pyproject.toml +1 -1
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/annotations/_common.py +50 -3
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/annotations/from_model.py +136 -108
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/annotations/orchestrator.py +6 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/annotations/sections.py +20 -1
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/builder.py +7 -6
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/drawing.py +78 -22
- draftwright-0.3.3/src/draftwright/repair.py +198 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/refactor_golden/centered_rebate.json +3 -3
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/refactor_golden/grid_plate.json +6 -6
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/refactor_golden/prismatic_ladder.json +3 -3
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/refactor_golden/scattered_plate.json +27 -27
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_carve_free_position_callers.py +7 -4
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_drawing_encapsulation.py +65 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_layout_cleanliness.py +82 -58
- draftwright-0.3.3/tests/test_occupancy_boxes.py +39 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_sheet_emit.py +7 -2
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_strip_layout.py +45 -10
- draftwright-0.3.3/tests/test_witness_label_reconciliation.py +56 -0
- draftwright-0.3.2/src/draftwright/repair.py +0 -91
- {draftwright-0.3.2 → draftwright-0.3.3}/.gitignore +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/LICENSE +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/README.md +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/skills/SKILL.md +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/__init__.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/_core.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/_geometry.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/analysis.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/annotate.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/annotations/__init__.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/annotations/holes.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/cli.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/compose.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/export.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/fits.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/fonts/__init__.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/intents.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/layout.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/linting/__init__.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/linting/coverage.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/linting/issues.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/linting/structural.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/linting/suggest.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/make_drawing.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/model/__init__.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/model/declare.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/model/detect.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/model/ir.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/model/planner.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/pmi.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/projection.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/recognition/__init__.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/recognition/_features.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/recognition/_record.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/recognition/chamfers.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/recognition/countersinks.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/recognition/fillets.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/recognition/flats.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/recognition/grooves.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/recognition/levels.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/recognition/plates.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/recognition/slots.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/recognition/turned.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/registry.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/score.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/sheet.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/sheet_dsl.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/src/draftwright/sheet_emit.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/_kernel.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/_layout_sig.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/refactor_golden/bracket_section.json +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/refactor_golden/chamfered.json +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/refactor_golden/filleted.json +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/refactor_golden/flange_dense.json +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/refactor_golden/grooved_shaft.json +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/refactor_golden/hex_bar.json +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/refactor_golden/holed_slot.json +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/refactor_golden/pocketed.json +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/refactor_golden/side_drilled.json +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/refactor_golden/turned_stepped.json +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_declare.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_detect_once.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_e2e_slice.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_e2e_standards.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_export_dxf_zoom.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_fillets_adjacency.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_fits.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_gdt_placement.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_import_boundaries.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_layout.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_layout_hypothesis.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_layout_property.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_lint_box_cache.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_lint_reconciliation.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_lint_structural.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_linting.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_make_drawing.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_object_aspects.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_part_model.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_pitch_dim_footprint.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_pmi.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_private_test_imports.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_recogniser_contract.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_recognition.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_refactor_golden.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_registry.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_render_seam.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_score.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_sheet_gdt.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_sheet_notes.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_sheet_of.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_sheet_tables.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_tolerances.py +0 -0
- {draftwright-0.3.2 → draftwright-0.3.3}/tests/test_turned_steps.py +0 -0
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.3.3 — 2026-07-18
|
|
4
|
+
|
|
5
|
+
The **layout-quality release**: the occupancy model gets honest about ink shape,
|
|
6
|
+
the last solver-invisible pass joins the corridor solve, and crossed labels shift
|
|
7
|
+
themselves clear. Also completes the #635 consolidation epic (ADR 0005 fully
|
|
8
|
+
executed; ADR 0009's no-invisible-occupant guarantee now holds by construction
|
|
9
|
+
for every auto-pass).
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Witness-crossing label shifts** (#690): a dimension label crossed by another
|
|
14
|
+
dimension's witness stroke now shifts along its own line to clear it — a
|
|
15
|
+
deterministic post-drain pass using the repair machinery (destination-aware,
|
|
16
|
+
axis-aligned only, pinned dims exempt).
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **L-shaped occupancy** (#685): placement obstacles decompose into per-stroke
|
|
21
|
+
boxes + label box (helpers ≥0.14 `segments`) instead of one hull, with
|
|
22
|
+
preset-aware arrowhead pads. View-corner contention between perpendicular
|
|
23
|
+
strips disappears by construction (a plate-thickness dim returns to its
|
|
24
|
+
natural strip); packing tightens. The cleanliness ratchet mirrors the model —
|
|
25
|
+
only transverse stroke crossings are exempt, so parallel overprints and label
|
|
26
|
+
contacts are detected honestly (twelve hull-artifact allowlist entries burned
|
|
27
|
+
down; the legitimate shared-datum running-dimension pairs are documented).
|
|
28
|
+
- **Height ladder joins the corridor solve** (#636): step heights + overall
|
|
29
|
+
height are corridor candidates (the leapfrog witness cursor survives as a
|
|
30
|
+
deterministic build-time chain; the overall height stacks outermost by
|
|
31
|
+
construction and packs tighter). An explicitly-requested crowded-step detail
|
|
32
|
+
view now *deliberately* demotes the overall-height dim when the sheet cannot
|
|
33
|
+
hold both (previously the carve dropped it silently); a Y-drilled hole's Z
|
|
34
|
+
location joins the front running ladder per the documented routing.
|
|
35
|
+
- **One typed `BuildState`** (#639): the drawing's build context (analysis,
|
|
36
|
+
part model, lint geometry caches) consolidates into a single object filled at
|
|
37
|
+
one builder site, single-writer-guarded; the render passes make zero private
|
|
38
|
+
`Drawing` reads (fail-closed empty allowlist).
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- A prismatic detail view no longer loses both the detail *and* the height dim
|
|
43
|
+
when space is tight (transactional demotion).
|
|
44
|
+
- The GD&T alternate-strip fallback defers until every corridor has drained, so
|
|
45
|
+
it can never preempt a sibling strip's reserved corner.
|
|
46
|
+
|
|
3
47
|
## v0.3.2 — 2026-07-18
|
|
4
48
|
|
|
5
49
|
The **performance release**: the #602 perf loop (six PRs) plus the
|
|
@@ -255,6 +255,52 @@ def clear_label_of_centerlines(label_bbox, centerlines, gap):
|
|
|
255
255
|
return target_x - lmin_x
|
|
256
256
|
|
|
257
257
|
|
|
258
|
+
def occupancy_boxes(o, stroke_pad=None):
|
|
259
|
+
"""*o*'s occupancy as a list of AABBs — decomposed, not one hull (#685).
|
|
260
|
+
|
|
261
|
+
An annotation's rendered hull includes large EMPTY corner regions (a dimension's
|
|
262
|
+
ink is L-shaped: witness lines + a dim-line band), and helpers ≥0.14's honest
|
|
263
|
+
tight-span rendering made those hulls big enough to collide structurally at view
|
|
264
|
+
corners while the inks stay disjoint. For an annotation exposing ``.segments``
|
|
265
|
+
(helpers ≥0.14 reports the drawn line pieces: witness lines, shafts, box
|
|
266
|
+
strokes), return one box per stroke — inflated by ``_STROKE_PAD`` to cover line
|
|
267
|
+
width plus the arrowheads at stroke junctions — plus its ``label_bbox``.
|
|
268
|
+
Anything else (leaders, hatch, title block) keeps its single hull box.
|
|
269
|
+
|
|
270
|
+
Consumers treat the result exactly like a list of hull boxes; the
|
|
271
|
+
perpendicular-band filter in the carve then does the rest — a witness sliver
|
|
272
|
+
carves only its own sliver, and an out-of-band dim-line band stops blocking a
|
|
273
|
+
sibling strip's corner entirely.
|
|
274
|
+
"""
|
|
275
|
+
segs = getattr(o, "segments", None)
|
|
276
|
+
if not segs:
|
|
277
|
+
b = _geom_box(o)
|
|
278
|
+
return [b] if b is not None else []
|
|
279
|
+
pad = _STROKE_PAD if stroke_pad is None else stroke_pad
|
|
280
|
+
out = []
|
|
281
|
+
for (x0, y0), (x1, y1) in segs:
|
|
282
|
+
out.append(
|
|
283
|
+
(
|
|
284
|
+
min(x0, x1) - pad,
|
|
285
|
+
min(y0, y1) - pad,
|
|
286
|
+
max(x0, x1) + pad,
|
|
287
|
+
max(y0, y1) + pad,
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
lb = getattr(o, "label_bbox", None)
|
|
291
|
+
if lb is not None:
|
|
292
|
+
out.append((lb[0], lb[1], lb[2], lb[3]))
|
|
293
|
+
return out
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# Fallback stroke inflation for decomposed occupancy: line_width/2 (~0.08) plus the
|
|
297
|
+
# arrowhead half-width at stroke junctions at DEFAULT presets. Arrow geometry scales
|
|
298
|
+
# with font_size (#688 review), so callers that know the draft derive the pad as
|
|
299
|
+
# max(_STROKE_PAD, draft.arrow_length / 2) — arrow half-LENGTH bounds the head's
|
|
300
|
+
# half-width (aspect < 1) AND its protrusion past an inside-arrow shaft trim (al/2).
|
|
301
|
+
_STROKE_PAD = 1.2
|
|
302
|
+
|
|
303
|
+
|
|
258
304
|
def strip_obstacles(dwg, view=None, *, crossable=()):
|
|
259
305
|
"""The COMPLETE occupancy for strip placement (ADR 0009): every placed
|
|
260
306
|
annotation's full rendered footprint, optionally restricted to *view*, minus
|
|
@@ -284,6 +330,9 @@ def strip_obstacles(dwg, view=None, *, crossable=()):
|
|
|
284
330
|
|
|
285
331
|
The occupancy source for the collect-then-solve carve — every migrated renderer's
|
|
286
332
|
``place_strip_candidates`` call wires this in (#321/#150/P3)."""
|
|
333
|
+
# Preset-aware stroke pad (#688 review): arrowheads scale with font_size.
|
|
334
|
+
al = getattr(getattr(dwg, "draft", None), "arrow_length", None)
|
|
335
|
+
pad = max(_STROKE_PAD, al / 2) if al else _STROKE_PAD
|
|
287
336
|
boxes = []
|
|
288
337
|
for name, o in dwg.iter_annotations():
|
|
289
338
|
if view is not None:
|
|
@@ -292,9 +341,7 @@ def strip_obstacles(dwg, view=None, *, crossable=()):
|
|
|
292
341
|
continue # owned by a different ortho view → its own (disjoint) block
|
|
293
342
|
if type(o).__name__ in crossable:
|
|
294
343
|
continue # this consumer may cross it (centre lines/marks for a dim)
|
|
295
|
-
|
|
296
|
-
if bb is not None:
|
|
297
|
-
boxes.append(bb)
|
|
344
|
+
boxes.extend(occupancy_boxes(o, stroke_pad=pad)) # decomposed, not one hull (#685)
|
|
298
345
|
return boxes
|
|
299
346
|
|
|
300
347
|
|
|
@@ -1846,46 +1846,40 @@ def _detect_step_repeat(step_zs, bb_min_z, bb_max_z, tol_frac=0.10):
|
|
|
1846
1846
|
|
|
1847
1847
|
def render_height_ladder(dwg, model, a, *, ctx, include_overall: bool = True) -> int:
|
|
1848
1848
|
"""Front-view right ladder: prismatic step heights (from `StepLevelFeature`)
|
|
1849
|
-
stacked inner→outer, then the overall height outermost —
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1849
|
+
stacked inner→outer, then the overall height outermost — registered as
|
|
1850
|
+
:class:`CorridorCandidate`s in the shared ``(front, right)`` corridor (#636;
|
|
1851
|
+
formerly the last solver-invisible `carve_free_position` auto-pass). The leapfrog
|
|
1852
|
+
witness cursor (#237) survives as a *build-time chain*: candidates share a
|
|
1853
|
+
``solved`` position map, and each dim's witness anchors on its nearest already-
|
|
1854
|
+
built predecessor's line (the view edge for the first) — build order follows the
|
|
1855
|
+
candidate order, so the chain is deterministic. Prediction uses an analytical
|
|
1856
|
+
footprint with the witness modelled from the view edge (a conservative
|
|
1857
|
+
over-cover along the stack axis; the accept-time real-box validation trims any
|
|
1858
|
+
drift). A turned part has no `StepLevelFeature`; a Z-turned part suppresses the
|
|
1859
|
+
overall height (the chain tiles it, ISO 129). Returns the count REGISTERED."""
|
|
1854
1860
|
draft = dwg.draft
|
|
1855
1861
|
FX, FZ = a.proj.front_x, a.proj.front_z
|
|
1856
|
-
|
|
1857
|
-
|
|
1862
|
+
edge2 = FX(a.bb.max.X) + 2
|
|
1863
|
+
zmin = FZ(a.bb.min.Z)
|
|
1864
|
+
tier = draft.font_size + 2 * draft.pad_around_text
|
|
1858
1865
|
step = next((f for f in model.features if f.kind == "step_level"), None)
|
|
1859
1866
|
levels = list(step.levels) if step is not None else []
|
|
1860
|
-
# Uniform staircase → one representative "N× rise" dim; else the per-step ladder
|
|
1861
|
-
# (legibility-gated). Turned parts have no levels, so neither fires.
|
|
1862
1867
|
rep = _detect_step_repeat(levels, a.bb.min.Z, a.bb.max.Z) if levels else None
|
|
1868
|
+
|
|
1869
|
+
# The chain, inner→outer: (name, page-z top, label, tier size, drop message).
|
|
1870
|
+
chain: list = []
|
|
1863
1871
|
if rep is not None:
|
|
1864
1872
|
n_rep, rise = rep
|
|
1865
1873
|
first = sorted(levels)[0]
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
if px is not None:
|
|
1869
|
-
dwg.add(
|
|
1870
|
-
_dim(
|
|
1871
|
-
(right_ladder, FZ(a.bb.min.Z), 0),
|
|
1872
|
-
(right_ladder, FZ(first), 0),
|
|
1873
|
-
"right",
|
|
1874
|
-
px - right_ladder,
|
|
1875
|
-
draft,
|
|
1876
|
-
label=f"{n_rep}× {_fmt(rise)}",
|
|
1877
|
-
),
|
|
1874
|
+
chain.append(
|
|
1875
|
+
(
|
|
1878
1876
|
"dim_step_typ",
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
n += 1
|
|
1883
|
-
else:
|
|
1884
|
-
ctx.record_issue(
|
|
1885
|
-
"error",
|
|
1886
|
-
"placement_unsatisfiable",
|
|
1877
|
+
FZ(first),
|
|
1878
|
+
f"{n_rep}× {_fmt(rise)}",
|
|
1879
|
+
_SLOT_DIM_STEP,
|
|
1887
1880
|
"representative step-height dimension dropped (front-view right strip full)",
|
|
1888
1881
|
)
|
|
1882
|
+
)
|
|
1889
1883
|
elif levels:
|
|
1890
1884
|
kept, n_close = _legible_steps(levels, a.bb.min.Z, a.SCALE)
|
|
1891
1885
|
if n_close:
|
|
@@ -1896,77 +1890,96 @@ def render_height_ladder(dwg, model, a, *, ctx, include_overall: bool = True) ->
|
|
|
1896
1890
|
"(use a detail view)",
|
|
1897
1891
|
)
|
|
1898
1892
|
# First-class escalation alongside the lint code (ADR 0009 Amdt 1, #351
|
|
1899
|
-
# PR-4b) — `_request_prismatic_detail` (sections.py)
|
|
1900
|
-
#
|
|
1901
|
-
# gate, which previously could queue a spurious detail even when the uniform-
|
|
1902
|
-
# staircase branch above already fully documented the part with one
|
|
1903
|
-
# representative dim (a real bug this routing fixes as a side effect).
|
|
1893
|
+
# PR-4b) — `_request_prismatic_detail` (sections.py) consumes this instead
|
|
1894
|
+
# of recomputing the legibility gate.
|
|
1904
1895
|
ctx.escalations.append(
|
|
1905
1896
|
Escalation(kind="step", view="front", feature=step, reason="illegible")
|
|
1906
1897
|
)
|
|
1907
1898
|
for col, z in enumerate(kept):
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
"(front-view right strip full)",
|
|
1899
|
+
chain.append(
|
|
1900
|
+
(
|
|
1901
|
+
f"dim_step_{col}",
|
|
1902
|
+
FZ(z),
|
|
1903
|
+
_fmt(z - a.bb.min.Z),
|
|
1904
|
+
_SLOT_DIM_STEP,
|
|
1905
|
+
"step-height dimension dropped (front-view right strip full)",
|
|
1916
1906
|
)
|
|
1917
|
-
break
|
|
1918
|
-
dwg.add(
|
|
1919
|
-
_dim(
|
|
1920
|
-
(right_ladder, FZ(a.bb.min.Z), 0),
|
|
1921
|
-
(right_ladder, FZ(z), 0),
|
|
1922
|
-
"right",
|
|
1923
|
-
px - right_ladder,
|
|
1924
|
-
draft,
|
|
1925
|
-
label=_fmt(z - a.bb.min.Z),
|
|
1926
|
-
),
|
|
1927
|
-
f"dim_step_{col}",
|
|
1928
|
-
view="front",
|
|
1929
1907
|
)
|
|
1930
|
-
right_ladder = px
|
|
1931
|
-
n += 1
|
|
1932
1908
|
|
|
1933
|
-
# Overall height — placed last so it sits OUTERMOST; suppressed for a Z-turned
|
|
1934
|
-
# part (its IR step-length chain already tiles the full height, ISO 129) and for
|
|
1935
|
-
# an X/Y rotational body (its Z extent IS the OD, dimensioned by render_rotational
|
|
1936
|
-
# — #222).
|
|
1937
1909
|
rot = next((f for f in model.features if f.kind == "rotational"), None)
|
|
1938
1910
|
od_is_height = rot is not None and rot.frame.axis in ("x", "y")
|
|
1939
1911
|
suppress_height = (not include_overall) or model.orientation == "z" or od_is_height
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1912
|
+
if not suppress_height:
|
|
1913
|
+
chain.append(
|
|
1914
|
+
(
|
|
1915
|
+
"dim_height",
|
|
1916
|
+
FZ(a.bb.max.Z),
|
|
1917
|
+
_fmt(a.z_size),
|
|
1918
|
+
_SLOT_DIM_HEIGHT,
|
|
1919
|
+
"overall height dimension dropped (front-view right strip full)",
|
|
1920
|
+
)
|
|
1921
|
+
)
|
|
1922
|
+
|
|
1923
|
+
names = [c[0] for c in chain]
|
|
1924
|
+
solved: dict[str, float] = {}
|
|
1925
|
+
for k, (name, ztop, label, _tsize, drop_msg) in enumerate(chain):
|
|
1926
|
+
|
|
1927
|
+
def _build(pos, name=name, ztop=ztop, label=label, k=k):
|
|
1928
|
+
base = edge2
|
|
1929
|
+
for pn in reversed(names[:k]): # nearest already-built predecessor's line
|
|
1930
|
+
if pn in solved:
|
|
1931
|
+
base = solved[pn]
|
|
1932
|
+
break
|
|
1933
|
+
solved[name] = pos
|
|
1934
|
+
return _dim((base, zmin, 0), (base, ztop, 0), "right", pos - base, draft, label=label)
|
|
1935
|
+
|
|
1936
|
+
def _foot(pos, ztop=ztop, label=label, k=k):
|
|
1937
|
+
# Predecessor-aware prediction (#689 review): the conservative
|
|
1938
|
+
# edge-anchored witness can falsely exhaust the strip when an inner
|
|
1939
|
+
# obstacle sits in the already-traversed region. Use the same solved
|
|
1940
|
+
# map the build chain uses — empty on the first evaluation (edge-based,
|
|
1941
|
+
# the prior behaviour), tight on later-segment retries.
|
|
1942
|
+
base = edge2
|
|
1943
|
+
for pn in reversed(names[:k]):
|
|
1944
|
+
if pn in solved:
|
|
1945
|
+
base = solved[pn]
|
|
1946
|
+
break
|
|
1947
|
+
if pos - base < 0.5: # degenerate guard: never model a zero-length offset
|
|
1948
|
+
base = edge2
|
|
1949
|
+
return dim_footprint(
|
|
1950
|
+
(base, zmin, 0), (base, ztop, 0), "right", pos - base, draft, label
|
|
1951
|
+
)
|
|
1952
|
+
|
|
1953
|
+
def _drop(nm, drop_msg=drop_msg, name=name):
|
|
1954
|
+
solved.pop(name, None)
|
|
1955
|
+
ctx.record_issue("error", "placement_unsatisfiable", drop_msg)
|
|
1956
|
+
|
|
1957
|
+
register_corridor(
|
|
1958
|
+
ctx,
|
|
1959
|
+
("front", "right"),
|
|
1945
1960
|
a.fv_zones.right,
|
|
1946
1961
|
"front",
|
|
1947
1962
|
"x",
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1963
|
+
tier,
|
|
1964
|
+
CorridorCandidate(
|
|
1965
|
+
name=name,
|
|
1966
|
+
build=_build,
|
|
1967
|
+
# Steps stack inner→outer in chain order; the overall height rides the
|
|
1968
|
+
# OVERALL subchain so it lands outermost by construction (as the
|
|
1969
|
+
# envelope dims do), replacing the old carve's outermost=True.
|
|
1970
|
+
order=(
|
|
1971
|
+
(_OVERALL_SUBCHAIN, 0, name)
|
|
1972
|
+
if name == "dim_height"
|
|
1973
|
+
else (_SIZE_SUBCHAIN, k, name)
|
|
1974
|
+
),
|
|
1975
|
+
on_place=lambda nm: None,
|
|
1976
|
+
on_drop=_drop,
|
|
1977
|
+
force=True, # principal dims: only a physically full strip drops them
|
|
1978
|
+
feature=step if name != "dim_height" else None,
|
|
1979
|
+
footprint=_foot,
|
|
1962
1980
|
),
|
|
1963
|
-
"dim_height",
|
|
1964
|
-
view="front",
|
|
1965
1981
|
)
|
|
1966
|
-
|
|
1967
|
-
elif not suppress_height:
|
|
1968
|
-
_log.warning("dim_height skipped: fv_zones.right strip full")
|
|
1969
|
-
return n
|
|
1982
|
+
return len(chain)
|
|
1970
1983
|
|
|
1971
1984
|
|
|
1972
1985
|
def render_step_positions(dwg, model, a, *, ctx) -> int:
|
|
@@ -2868,29 +2881,44 @@ def render_gdt(dwg, model, a, *, ctx) -> int:
|
|
|
2868
2881
|
):
|
|
2869
2882
|
# Fallthrough (#481): the declared/derived side is full — try the OPPOSITE side of
|
|
2870
2883
|
# the same view before dropping, so a congested default still places somewhere
|
|
2871
|
-
# legible rather than vanishing.
|
|
2872
|
-
#
|
|
2873
|
-
#
|
|
2874
|
-
#
|
|
2875
|
-
#
|
|
2876
|
-
#
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
"
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2884
|
+
# legible rather than vanishing. DEFERRED via ctx.post_drain (#636, the plate
|
|
2885
|
+
# pattern): the carve then runs after EVERY corridor has drained, so it cannot
|
|
2886
|
+
# preempt a corner a later sibling's force candidate needs. Force semantics
|
|
2887
|
+
# (no corridor-cross check) match the primary path, BUT reject a spot over the
|
|
2888
|
+
# (not-yet-placed) title block — a below/right strip runs into it, and the
|
|
2889
|
+
# carve can't see it (#481 review).
|
|
2890
|
+
def _retry(
|
|
2891
|
+
nm=nm,
|
|
2892
|
+
_v=_v,
|
|
2893
|
+
_s=_s,
|
|
2894
|
+
_zones=_zones,
|
|
2895
|
+
_px=_px,
|
|
2896
|
+
_py=_py,
|
|
2897
|
+
_hz=_hz,
|
|
2898
|
+
_sz=_sz,
|
|
2899
|
+
_bld=_bld,
|
|
2900
|
+
_feat=_feat,
|
|
2901
|
+
_tb=_tb,
|
|
2902
|
+
):
|
|
2903
|
+
alt = {"above": "below", "below": "above", "left": "right", "right": "left"}[_s]
|
|
2904
|
+
alt_strip = getattr(_zones, alt, None)
|
|
2905
|
+
if alt_strip is not None:
|
|
2906
|
+
axis2 = "y" if alt in ("above", "below") else "x"
|
|
2907
|
+
extent = _sz[1] if axis2 == "y" else _sz[0] # the glyph's stacking-axis size
|
|
2908
|
+
perp = (_px, _px + _sz[0]) if _hz else (_py - _sz[1] / 2, _py + _sz[1] / 2)
|
|
2909
|
+
pos = carve_free_position(dwg, alt_strip, _v, axis2, max(tier, extent), perp)
|
|
2910
|
+
if pos is not None:
|
|
2911
|
+
dim = _bld(pos)
|
|
2912
|
+
if not _box_hits(_anno_box(dim), (_tb,)): # clear of the title block
|
|
2913
|
+
dwg.add(dim, nm, view=_v, feature=_feat) # alternate side
|
|
2914
|
+
return
|
|
2915
|
+
ctx.record_issue(
|
|
2916
|
+
"warning",
|
|
2917
|
+
"gdt_dropped",
|
|
2918
|
+
f"{nm} not placed (no room in the {_v} {_s} strip or its opposite)",
|
|
2919
|
+
)
|
|
2920
|
+
|
|
2921
|
+
ctx.post_drain.append(_retry)
|
|
2894
2922
|
|
|
2895
2923
|
register_corridor(
|
|
2896
2924
|
ctx,
|
|
@@ -72,6 +72,7 @@ from draftwright.model import (
|
|
|
72
72
|
plan_dimensions,
|
|
73
73
|
plan_sections,
|
|
74
74
|
)
|
|
75
|
+
from draftwright.repair import reconcile_witness_labels
|
|
75
76
|
|
|
76
77
|
|
|
77
78
|
def _wrap_rows(header, data, ncols):
|
|
@@ -356,6 +357,11 @@ def _auto_annotate(dwg, a: Analysis, *, detail_view: bool = False):
|
|
|
356
357
|
# (ADR 0009 end state, #345/#346/#393) — dedup coincident spans, order the ladder —
|
|
357
358
|
# BEFORE the section/detail views so they see the placed ladder as an obstacle.
|
|
358
359
|
drain_corridors(ctx, dwg)
|
|
360
|
+
# (#690) Witness-crossing label reconciliation — after every corridor (and its
|
|
361
|
+
# deferred fallthroughs) has placed: shift any dim label a FOREIGN transverse
|
|
362
|
+
# stroke crosses, along its own line, via the repair machinery. Deterministic,
|
|
363
|
+
# runs in both build paths.
|
|
364
|
+
reconcile_witness_labels(dwg)
|
|
359
365
|
|
|
360
366
|
# Turned/circlip-groove callouts (#148c): {width} WIDE × ø{dia} via a leader off each
|
|
361
367
|
# groove. A groove is a secondary leader-callout on a turned shaft — exactly where the
|
|
@@ -562,7 +562,26 @@ def _resolve_details(dwg, a: Analysis, *, ctx) -> None:
|
|
|
562
562
|
_log.info("detail request '%s' dropped: detail letters A–H exhausted", req.kind)
|
|
563
563
|
continue
|
|
564
564
|
letter = _DETAIL_LETTERS[n_placed]
|
|
565
|
-
|
|
565
|
+
placed = _render_detail(dwg, a, req, f"detail_{letter.lower()}", letter)
|
|
566
|
+
if not placed and req.kind == "prismatic-steps" and "dim_height" in dwg.annotations():
|
|
567
|
+
# (#636) The user's explicit detail request outranks the overall-height
|
|
568
|
+
# dim: pre-migration, the solver-invisible carve silently dropped
|
|
569
|
+
# dim_height on exactly these crowded parts, and the detail took that
|
|
570
|
+
# room. The corridor solve now places dim_height when it fits — so on a
|
|
571
|
+
# no-room prismatic detail, demote it DELIBERATELY (same outcome, now
|
|
572
|
+
# explicit and logged) and retry the detail once.
|
|
573
|
+
hview = dwg.view_of("dim_height")
|
|
574
|
+
hobj = dwg.remove("dim_height")
|
|
575
|
+
placed = _render_detail(dwg, a, req, f"detail_{letter.lower()}", letter)
|
|
576
|
+
if placed:
|
|
577
|
+
_log.warning(
|
|
578
|
+
"dim_height demoted: the requested crowded-step detail view takes its room"
|
|
579
|
+
)
|
|
580
|
+
else:
|
|
581
|
+
# Transactional (#689 review): the detail may fail for reasons other
|
|
582
|
+
# than dim_height's room — restore it rather than losing BOTH.
|
|
583
|
+
dwg.add(hobj, "dim_height", view=hview)
|
|
584
|
+
if placed:
|
|
566
585
|
n_placed += 1
|
|
567
586
|
|
|
568
587
|
|
|
@@ -265,14 +265,13 @@ def _assemble(a, out, assembly, detail_view, auto_dims, model=None, decorations=
|
|
|
265
265
|
cyls=a.cyls,
|
|
266
266
|
assembly=assembly,
|
|
267
267
|
)
|
|
268
|
-
dwg._analysis = a # expose analysis namespace for testing and future strip access
|
|
269
268
|
# Detect the IR here — before the auto_dims gate — so dwg.model() and feature edits
|
|
270
269
|
# work even in manual mode (#398). _auto_annotate reads this attached model rather
|
|
271
270
|
# than rebuilding. On a repack this runs again on the pass-2 drawing (freshness).
|
|
272
271
|
# Detected path: reuse the model _analyse already built for sizing (#584 WP1 A) —
|
|
273
272
|
# detectors run once per build (ADR 0008 Amdt 5, #602). build_model(a) remains the
|
|
274
273
|
# fallback for a manually-constructed Analysis with no stored model.
|
|
275
|
-
|
|
274
|
+
pm = (
|
|
276
275
|
_coerce_model(model, a.part, decorations)
|
|
277
276
|
if model is not None
|
|
278
277
|
else (a.model if a.model is not None else build_model(a))
|
|
@@ -283,23 +282,25 @@ def _assemble(a, out, assembly, detail_view, auto_dims, model=None, decorations=
|
|
|
283
282
|
# (rot furniture). Synthesise it from the (unconditional) analysis so a declared /
|
|
284
283
|
# emitted-script turned part reproduces the detected drawing (#472). Gated on the
|
|
285
284
|
# caller not having declared one, so an explicit choice wins.
|
|
286
|
-
pm = dwg._part_model
|
|
287
285
|
if not any(f.kind == "rotational" for f in pm.features):
|
|
288
286
|
rot = build_rotational_feature(a)
|
|
289
287
|
if rot is not None:
|
|
290
|
-
|
|
288
|
+
pm = replace(pm, features=[*pm.features, rot])
|
|
291
289
|
# PMI (STEP AP242) is likewise detection-sourced, so a declared / emitted-script model
|
|
292
290
|
# carries none. When PMI annotation is on, synthesise the same imported drafting
|
|
293
291
|
# annotations detection would (render_pmi reads them off the model, gated on a.pmi_mode)
|
|
294
292
|
# so a re-run reproduces the PMI dims (#472). Gated on the caller not having declared
|
|
295
293
|
# imported authored annotations, so an explicit set wins.
|
|
296
|
-
pm = dwg._part_model
|
|
297
294
|
if a.pmi_mode == "annotate" and not any(
|
|
298
295
|
f.kind in ("authored_dimension", "pmi") for f in pm.features
|
|
299
296
|
):
|
|
300
297
|
pmi_feats = build_pmi_features(a.pmi, a.part.bounding_box())
|
|
301
298
|
if pmi_feats:
|
|
302
|
-
|
|
299
|
+
pm = replace(pm, features=[*pm.features, *pmi_feats])
|
|
300
|
+
# ADR 0005 §2 (#639): the ONE build-context attachment — analysis + finished model
|
|
301
|
+
# in a single typed BuildState; the compat properties on Drawing read through it.
|
|
302
|
+
dwg._build.analysis = a
|
|
303
|
+
dwg._build.part_model = pm
|
|
303
304
|
dwg._model_declared = model is not None # ADR 0011 #448: gate model-driven hole render
|
|
304
305
|
|
|
305
306
|
part_s = a.part.scale(a.SCALE)
|