draftwright 0.3.5__tar.gz → 0.3.6__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.
Files changed (136) hide show
  1. {draftwright-0.3.5 → draftwright-0.3.6}/CHANGELOG.md +44 -1
  2. {draftwright-0.3.5 → draftwright-0.3.6}/PKG-INFO +1 -1
  3. {draftwright-0.3.5 → draftwright-0.3.6}/pyproject.toml +1 -1
  4. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/annotations/sections.py +17 -0
  5. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/builder.py +76 -2
  6. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/cli.py +7 -0
  7. draftwright-0.3.6/tests/test_dense_sheet_canary.py +99 -0
  8. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_layout_hypothesis.py +30 -1
  9. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_make_drawing.py +108 -0
  10. {draftwright-0.3.5 → draftwright-0.3.6}/.gitignore +0 -0
  11. {draftwright-0.3.5 → draftwright-0.3.6}/LICENSE +0 -0
  12. {draftwright-0.3.5 → draftwright-0.3.6}/README.md +0 -0
  13. {draftwright-0.3.5 → draftwright-0.3.6}/docs/adr/README.md +0 -0
  14. {draftwright-0.3.5 → draftwright-0.3.6}/skills/SKILL.md +0 -0
  15. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/__init__.py +0 -0
  16. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/_core.py +0 -0
  17. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/_geometry.py +0 -0
  18. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/analysis.py +0 -0
  19. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/annotate.py +0 -0
  20. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/annotations/__init__.py +0 -0
  21. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/annotations/_common.py +0 -0
  22. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/annotations/balloons.py +0 -0
  23. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/annotations/from_model.py +0 -0
  24. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/annotations/holes.py +0 -0
  25. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/annotations/orchestrator.py +0 -0
  26. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/compose.py +0 -0
  27. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/drawing.py +0 -0
  28. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/export.py +0 -0
  29. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/fits.py +0 -0
  30. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
  31. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
  32. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
  33. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
  34. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/fonts/__init__.py +0 -0
  35. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/intents.py +0 -0
  36. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/layout.py +0 -0
  37. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/linting/__init__.py +0 -0
  38. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/linting/coverage.py +0 -0
  39. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/linting/issues.py +0 -0
  40. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/linting/structural.py +0 -0
  41. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/linting/suggest.py +0 -0
  42. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/make_drawing.py +0 -0
  43. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/model/__init__.py +0 -0
  44. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/model/declare.py +0 -0
  45. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/model/detect.py +0 -0
  46. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/model/ir.py +0 -0
  47. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/model/planner.py +0 -0
  48. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/pmi.py +0 -0
  49. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/projection.py +0 -0
  50. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/recognition/__init__.py +0 -0
  51. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/recognition/_features.py +0 -0
  52. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/recognition/_record.py +0 -0
  53. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/recognition/chamfers.py +0 -0
  54. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/recognition/countersinks.py +0 -0
  55. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/recognition/fillets.py +0 -0
  56. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/recognition/flats.py +0 -0
  57. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/recognition/grooves.py +0 -0
  58. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/recognition/levels.py +0 -0
  59. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/recognition/plates.py +0 -0
  60. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/recognition/slots.py +0 -0
  61. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/recognition/turned.py +0 -0
  62. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/registry.py +0 -0
  63. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/repair.py +0 -0
  64. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/score.py +0 -0
  65. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/sheet.py +0 -0
  66. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/sheet_dsl.py +0 -0
  67. {draftwright-0.3.5 → draftwright-0.3.6}/src/draftwright/sheet_emit.py +0 -0
  68. {draftwright-0.3.5 → draftwright-0.3.6}/tests/_kernel.py +0 -0
  69. {draftwright-0.3.5 → draftwright-0.3.6}/tests/_layout_sig.py +0 -0
  70. {draftwright-0.3.5 → draftwright-0.3.6}/tests/fixtures/grm03_thumbwheel_drive_screw.step +0 -0
  71. {draftwright-0.3.5 → draftwright-0.3.6}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
  72. {draftwright-0.3.5 → draftwright-0.3.6}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
  73. {draftwright-0.3.5 → draftwright-0.3.6}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
  74. {draftwright-0.3.5 → draftwright-0.3.6}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
  75. {draftwright-0.3.5 → draftwright-0.3.6}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
  76. {draftwright-0.3.5 → draftwright-0.3.6}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
  77. {draftwright-0.3.5 → draftwright-0.3.6}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
  78. {draftwright-0.3.5 → draftwright-0.3.6}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
  79. {draftwright-0.3.5 → draftwright-0.3.6}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
  80. {draftwright-0.3.5 → draftwright-0.3.6}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
  81. {draftwright-0.3.5 → draftwright-0.3.6}/tests/refactor_golden/bracket_section.json +0 -0
  82. {draftwright-0.3.5 → draftwright-0.3.6}/tests/refactor_golden/centered_rebate.json +0 -0
  83. {draftwright-0.3.5 → draftwright-0.3.6}/tests/refactor_golden/chamfered.json +0 -0
  84. {draftwright-0.3.5 → draftwright-0.3.6}/tests/refactor_golden/filleted.json +0 -0
  85. {draftwright-0.3.5 → draftwright-0.3.6}/tests/refactor_golden/flange_dense.json +0 -0
  86. {draftwright-0.3.5 → draftwright-0.3.6}/tests/refactor_golden/grid_plate.json +0 -0
  87. {draftwright-0.3.5 → draftwright-0.3.6}/tests/refactor_golden/grooved_shaft.json +0 -0
  88. {draftwright-0.3.5 → draftwright-0.3.6}/tests/refactor_golden/hex_bar.json +0 -0
  89. {draftwright-0.3.5 → draftwright-0.3.6}/tests/refactor_golden/holed_slot.json +0 -0
  90. {draftwright-0.3.5 → draftwright-0.3.6}/tests/refactor_golden/pocketed.json +0 -0
  91. {draftwright-0.3.5 → draftwright-0.3.6}/tests/refactor_golden/prismatic_ladder.json +0 -0
  92. {draftwright-0.3.5 → draftwright-0.3.6}/tests/refactor_golden/scattered_plate.json +0 -0
  93. {draftwright-0.3.5 → draftwright-0.3.6}/tests/refactor_golden/side_drilled.json +0 -0
  94. {draftwright-0.3.5 → draftwright-0.3.6}/tests/refactor_golden/turned_stepped.json +0 -0
  95. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_architecture_docs.py +0 -0
  96. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_carve_free_position_callers.py +0 -0
  97. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_declare.py +0 -0
  98. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_detect_once.py +0 -0
  99. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_drawing_encapsulation.py +0 -0
  100. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_e2e_slice.py +0 -0
  101. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_e2e_standards.py +0 -0
  102. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_export_dxf_zoom.py +0 -0
  103. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_fillets_adjacency.py +0 -0
  104. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_fits.py +0 -0
  105. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_gdt_placement.py +0 -0
  106. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_import_boundaries.py +0 -0
  107. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_layout.py +0 -0
  108. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_layout_cleanliness.py +0 -0
  109. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_layout_property.py +0 -0
  110. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_lint_box_cache.py +0 -0
  111. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_lint_reconciliation.py +0 -0
  112. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_lint_structural.py +0 -0
  113. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_linting.py +0 -0
  114. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_object_aspects.py +0 -0
  115. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_occupancy_boxes.py +0 -0
  116. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_part_model.py +0 -0
  117. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_pitch_dim_footprint.py +0 -0
  118. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_pmi.py +0 -0
  119. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_private_test_imports.py +0 -0
  120. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_recogniser_contract.py +0 -0
  121. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_recognition.py +0 -0
  122. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_refactor_golden.py +0 -0
  123. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_registry.py +0 -0
  124. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_render_seam.py +0 -0
  125. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_score.py +0 -0
  126. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_script_detail_parity.py +0 -0
  127. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_sheet_emit.py +0 -0
  128. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_sheet_gdt.py +0 -0
  129. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_sheet_notes.py +0 -0
  130. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_sheet_of.py +0 -0
  131. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_sheet_tables.py +0 -0
  132. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_solve_trace.py +0 -0
  133. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_strip_layout.py +0 -0
  134. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_tolerances.py +0 -0
  135. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_turned_steps.py +0 -0
  136. {draftwright-0.3.5 → draftwright-0.3.6}/tests/test_witness_label_reconciliation.py +0 -0
@@ -1,6 +1,49 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## v0.3.6 — 2026-07-21
4
+
5
+ ### Fixed
6
+
7
+ - **`Sheet.step(boss)` on a prismatic part fails loudly instead of silently
8
+ dropping the height** (#631): declaring a turned `.step()` at a cylinder that is
9
+ really an external boss set `orientation="z"`, which made `render_height_ladder`
10
+ suppress the overall-height dim on the assumption the OD/step chain conveyed it —
11
+ but on a prismatic box that chain never renders, so the height vanished and
12
+ nothing replaced it (11 → 10 annotations). The build now detects a `StepFeature`
13
+ coincident with a `BossFeature` (same axis, origin, diameter) — a combination
14
+ that can never be legitimate — and raises a clear `ValueError` pointing to
15
+ `.boss()` (which renders its own ø and height per #632). Legitimate turned shafts
16
+ have steps but no coincident boss, so they are unaffected.
17
+ - **`detail_view=True` no longer a silent no-op when the detail can't be placed**
18
+ (#630): a part whose lint recommends a detail view (`step_dim_dropped`) but whose
19
+ crowded band is full-width (e.g. a shelled cover's stacked face levels) can't be
20
+ enlarged legibly *and* still fit alongside the main views — so `detail_view=True`
21
+ produced a drawing byte-identical to `False`, silently ignoring the opt-in. It
22
+ now records a `detail_unplaceable` warning naming the reason and an actionable
23
+ remedy (dimension manually / move onto its own sheet), so the opt-in is
24
+ observable. Parts whose detail *does* fit are unchanged.
25
+ - **Imperative `--script` round-trips the title-block fields + sheet furniture**
26
+ (#775): the imperative flavour reproduced only title/number/tolerance/drawn_by/
27
+ scale/page, dropping the `material`/`date`/`revision`/`company` fields (#766) and
28
+ the `frame`/`zones`/`projection` furniture (#767/#768/#769) — so a regenerated
29
+ imperative script didn't match the direct CLI. They now ride the emitted script's
30
+ cog config block and its `build_drawing(...)` call (the Sheet flavour already
31
+ round-tripped them), and the CLI forwards them to `generate_script`.
32
+
33
+ ### Internal
34
+
35
+ - **Fast-tier dense-sheet canary for the #733/#734 strip-pressure regression**
36
+ (#737): a synthetic contended-front-strip fixture that drops a step-height
37
+ principal dim on the pre-#734 commit and stays clean on `main` — closing the
38
+ fast-tier gap the #733 regression fell through (only the CI-only slow CTC
39
+ fixtures exercised real strip pressure before).
40
+ - **Layout-hypothesis fuzz tier is now a reproducible PR gate** (#692):
41
+ `derandomize=True` makes every run generate the same fixed example set (with
42
+ `print_blob` for paste-able reproduction), so a failure recurs deterministically
43
+ instead of surfacing on a seed-dependent minority of runs; exploratory fuzzing
44
+ moves behind `DRAFTWRIGHT_FUZZ_EXPLORE=1`. The two `test_make_drawing.py` xdist
45
+ observations (#669) were investigated and found non-reproducible — both tests are
46
+ non-flaky by construction/measurement on current `main`.
4
47
 
5
48
  ## v0.3.5 — 2026-07-20
6
49
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: draftwright
3
- Version: 0.3.5
3
+ Version: 0.3.6
4
4
  Summary: Automated technical-drawing generation for build123d
5
5
  Project-URL: Homepage, https://github.com/pzfreo/draftwright
6
6
  Project-URL: Repository, https://github.com/pzfreo/draftwright
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "draftwright"
7
- version = "0.3.5"
7
+ version = "0.3.6"
8
8
  description = "Automated technical-drawing generation for build123d"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -597,6 +597,23 @@ def _resolve_details(dwg, a: Analysis, *, ctx) -> None:
597
597
  dwg.pin(hname)
598
598
  if placed:
599
599
  n_placed += 1
600
+ elif req.kind == "prismatic-steps":
601
+ # (#630) The prismatic-steps request is the one queued only under the
602
+ # build_drawing(detail_view=True) opt-in (_request_prismatic_detail's gate),
603
+ # so a bail-out here means that opt-in produced nothing. Say so — with an
604
+ # actionable remedy — rather than returning a drawing byte-identical to
605
+ # detail_view=False (a full-width crowded band, e.g. a shelled cover's stacked
606
+ # face levels, can't be enlarged legibly and still fit alongside the main views).
607
+ # The automatic turned-head requests (queued unconditionally by
608
+ # render_step_lengths) are NOT opt-in — their bail-out is the normal path (the
609
+ # main view locates the head/block inline), so they stay silent.
610
+ ctx.record_issue(
611
+ "warning",
612
+ "detail_unplaceable",
613
+ f"{req.kind} detail view requested but could not be placed legibly on this "
614
+ "sheet (the crowded band is too wide to enlarge and still fit); dimension the "
615
+ "feature manually or move it onto its own sheet",
616
+ )
600
617
 
601
618
 
602
619
  def _overall_height_name(dwg, a: Analysis) -> str | None:
@@ -287,6 +287,33 @@ def _assemble(
287
287
  rot = build_rotational_feature(a)
288
288
  if rot is not None:
289
289
  pm = replace(pm, features=[*pm.features, rot])
290
+ # A z step declares a segment of a z-turned profile; the height ladder then suppresses
291
+ # the overall height on the premise the step-length chain conveys it (from_model
292
+ # render_height_ladder). That premise holds only if the declared steps span the part's
293
+ # FULL z-extent — a boss, or even a stepped boss on a plate, declared via .step()
294
+ # leaves the bulk unspanned, so suppression silently drops the overall height (#631).
295
+ # Guard on that exact condition rather than a classifier proxy (is_rotational / prof
296
+ # both have blind spots): raise when the z-steps don't reach both ends of the part.
297
+ z_steps = [f for f in pm.features if isinstance(f, StepFeature) and f.frame.axis == "z"]
298
+ if pm.orientation == "z" and z_steps:
299
+ tol = 1e-3 * max(a.z_size, 1.0) # small absolute float epsilon, floored
300
+ # The step lengths convey the overall height only if the steps TILE the z-extent
301
+ # contiguously — a single reach to each end isn't enough (an interior gap would
302
+ # still leave height unconveyed). Walk the spans low→high, extending coverage.
303
+ spans = sorted(
304
+ (min(p0[2], p1[2]), max(p0[2], p1[2])) for f in z_steps for (p0, p1) in [f.span]
305
+ )
306
+ covered = a.bb.min.Z
307
+ for lo, hi in spans:
308
+ if lo <= covered + tol:
309
+ covered = max(covered, hi)
310
+ if spans[0][0] > a.bb.min.Z + tol or covered < a.bb.max.Z - tol:
311
+ raise ValueError(
312
+ "step() declares a segment of a turned profile, but the declared steps "
313
+ "don't span this part's full height — it is not a turned (rotational) "
314
+ "body. For a boss (an external cylinder on a prismatic part) use .boss() "
315
+ "— it renders its own ø and height."
316
+ )
290
317
  # PMI (STEP AP242) is likewise detection-sourced, so a declared / emitted-script model
291
318
  # carries none. When PMI annotation is on, synthesise the same imported drafting
292
319
  # annotations detection would (render_pmi reads them off the model, gated on a.pmi_mode)
@@ -975,6 +1002,13 @@ def _write_script(
975
1002
  f"NUMBER = {a.number!r}",
976
1003
  f"TOLERANCE = {a.tolerance!r}",
977
1004
  f"DRAWN_BY = {a.drawn_by!r}",
1005
+ f"MATERIAL = {a.material!r}",
1006
+ f"DATE = {a.date!r}",
1007
+ f"REVISION = {a.revision!r}",
1008
+ f"COMPANY = {a.company!r}",
1009
+ f"FRAME = {a.frame!r}",
1010
+ f"ZONES = {a.zones!r}",
1011
+ f"PROJECTION = {a.projection!r}",
978
1012
  f"PMI = {a.pmi_mode!r}",
979
1013
  f"SCALE = {scale!r}",
980
1014
  f"PAGE = {page!r}",
@@ -989,6 +1023,13 @@ def _write_script(
989
1023
  f"_NUMBER = {a.number!r}\n"
990
1024
  f"_TOLERANCE = {a.tolerance!r}\n"
991
1025
  f"_DRAWN_BY = {a.drawn_by!r}\n"
1026
+ f"_MATERIAL = {a.material!r}\n"
1027
+ f"_DATE = {a.date!r}\n"
1028
+ f"_REVISION = {a.revision!r}\n"
1029
+ f"_COMPANY = {a.company!r}\n"
1030
+ f"_FRAME = {a.frame!r} # draw a sheet border (#767)\n"
1031
+ f"_ZONES = {a.zones!r} # ISO 5457 zone ruler (implies frame, #768)\n"
1032
+ f"_PROJECTION = {a.projection!r} # 'third' | 'first' | None (#769)\n"
992
1033
  f"_PMI = {a.pmi_mode!r} # 'off' | 'report' | 'annotate'\n"
993
1034
  f"_SCALE = {scale!r} # None = auto; e.g. 5 for 5:1, 0.5 for 1:2\n"
994
1035
  f"_PAGE = {page!r} # None = auto; e.g. 'A3' or (297, 210)\n"
@@ -997,7 +1038,11 @@ def _write_script(
997
1038
  " for _k, _v in [\n"
998
1039
  " ('STEP_FILE', repr(_STEP_FILE)), ('TITLE', repr(_TITLE)),\n"
999
1040
  " ('NUMBER', repr(_NUMBER)), ('TOLERANCE', repr(_TOLERANCE)),\n"
1000
- " ('DRAWN_BY', repr(_DRAWN_BY)), ('PMI', repr(_PMI)),\n"
1041
+ " ('DRAWN_BY', repr(_DRAWN_BY)), ('MATERIAL', repr(_MATERIAL)),\n"
1042
+ " ('DATE', repr(_DATE)), ('REVISION', repr(_REVISION)),\n"
1043
+ " ('COMPANY', repr(_COMPANY)), ('FRAME', repr(_FRAME)),\n"
1044
+ " ('ZONES', repr(_ZONES)), ('PROJECTION', repr(_PROJECTION)),\n"
1045
+ " ('PMI', repr(_PMI)),\n"
1001
1046
  " ('SCALE', repr(_SCALE)), ('PAGE', repr(_PAGE)),\n"
1002
1047
  " ]:\n"
1003
1048
  " cog.outl(f'{_k} = {_v}')\n"
@@ -1042,6 +1087,13 @@ def _write_script(
1042
1087
  " number=NUMBER,\n"
1043
1088
  " tolerance=TOLERANCE,\n"
1044
1089
  " drawn_by=DRAWN_BY,\n"
1090
+ " material=MATERIAL,\n"
1091
+ " date=DATE,\n"
1092
+ " revision=REVISION,\n"
1093
+ " company=COMPANY,\n"
1094
+ " frame=FRAME,\n"
1095
+ " zones=ZONES,\n"
1096
+ " projection=PROJECTION,\n"
1045
1097
  " pmi=PMI,\n"
1046
1098
  " scale=SCALE,\n"
1047
1099
  " page=PAGE,\n"
@@ -1103,6 +1155,13 @@ def generate_script(
1103
1155
  pmi: Literal["off", "report", "annotate"] = "off",
1104
1156
  scale: float | None = None,
1105
1157
  page: str | None = None,
1158
+ material: str = "",
1159
+ date: str = "",
1160
+ revision: str = "A",
1161
+ company: str = "",
1162
+ frame: bool = False,
1163
+ zones: bool = False,
1164
+ projection: str | None = None,
1106
1165
  formats: Sequence[str] = ("pdf",),
1107
1166
  ) -> str:
1108
1167
  """Generate an editable Cog-enabled drawing script from a STEP file.
@@ -1131,7 +1190,22 @@ def generate_script(
1131
1190
  # here too would crash generation on an out-of-range value (e.g. --script --scale
1132
1191
  # 0.001 / --page A9) instead of writing the script and deferring — inconsistent with
1133
1192
  # a large unfittable scale, which already defers (review #401).
1134
- a = _analyse(step_file, title, number, tolerance, drawn_by, out, pmi=pmi)
1193
+ a = _analyse(
1194
+ step_file,
1195
+ title,
1196
+ number,
1197
+ tolerance,
1198
+ drawn_by,
1199
+ out,
1200
+ pmi=pmi,
1201
+ material=material,
1202
+ date=date,
1203
+ revision=revision,
1204
+ company=company,
1205
+ frame=frame,
1206
+ zones=zones,
1207
+ projection=projection,
1208
+ )
1135
1209
  return _write_script(a, scale=scale, page=page, formats=formats)
1136
1210
 
1137
1211
 
@@ -235,6 +235,13 @@ def main(
235
235
  number=number,
236
236
  tolerance=tolerance,
237
237
  drawn_by=drawn_by,
238
+ material=material,
239
+ date=date,
240
+ revision=revision,
241
+ company=company,
242
+ frame=frame,
243
+ zones=zones,
244
+ projection=projection or None,
238
245
  pmi=pmi.value,
239
246
  scale=scale,
240
247
  page=page,
@@ -0,0 +1,99 @@
1
+ """Fast-tier dense-sheet canary for the #733 strip-pressure regression class (#737).
2
+
3
+ #733 (the #636 regression, PR #689 "height ladder joins the corridor solve") dropped the
4
+ step-height and overall-height **principal** dimensions on the NIST CTC-02/04 fixtures —
5
+ ``placement_unsatisfiable`` **error** lint, *"front-view right strip full"* — and sat on
6
+ ``main`` for 12 hours because only the **slow** CTC fixtures exercise real strip pressure,
7
+ and the slow tier runs post-merge only (#153). The PR gate never saw it.
8
+
9
+ PR #734 fixed it *by construction*: **decoration places after the drain, so principal
10
+ dims win** — on an over-full front-view right strip the height/step chain is placed first
11
+ and the decoration (pocket/fillet dims) is what gets shed, never a principal.
12
+
13
+ This is the fast-tier tripwire for that guarantee. The synthetic part below double-loads
14
+ the same strip the CTC cases do: a four-step tower stacks a step-height chain + overall
15
+ height on the front-view right strip, and blind pockets milled into the **front face** put
16
+ competing pocket callouts on the same view. Pre-#734 the register-then-drain ordering let
17
+ an immediate pocket callout claim strip room a principal needed, so a step-height dim was
18
+ dropped (``placement_unsatisfiable``); post-#734 the principals drain and place first, so
19
+ every principal survives and the sheet stays free of error-severity lint. Build time is a
20
+ couple of seconds, so it runs on every PR.
21
+
22
+ It is a genuine regression test, empirically calibrated against the fix, not a smoke test:
23
+ run against the pre-#734 commit ``190e9ba`` (#636/PR #689) this exact part drops a
24
+ step-height dimension — ``placement_unsatisfiable`` "front-view right strip full", the #733
25
+ error — while on ``main`` (post-#734) every principal survives and only pocket decoration
26
+ is shed. So a revert of #734's drain-ordering (or any change that lets decoration outrank a
27
+ principal on a full strip) trips this test on the PR gate instead of post-merge.
28
+
29
+ Scope (ADR 0014; #735 — test-only, no solve rewrite): the slow CTC pair + golden corpus
30
+ remain the authoritative oracle; this narrows the *fast-tier* gap #733 fell through. The
31
+ build is a pure function of its input (guarded by ``test_layout_cleanliness``) and fonts
32
+ are path-pinned (ADR 0006), so the placed set is deterministic across the CI matrix.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import pytest
38
+ from build123d import Box, Pos
39
+
40
+ from draftwright import build_drawing
41
+
42
+ _N_POCKETS = 8
43
+
44
+
45
+ def _contended_front_strip_part():
46
+ """A part whose front-view right strip is contended: a step-height chain + overall
47
+ height (the principals) competing with front-face pocket callouts (decoration).
48
+
49
+ The four-step tower stacks the height chain on the front-right strip; the blind pockets
50
+ milled into the front face (``-Y``) add competing pocket callouts to the same view.
51
+ Pre-#734 the greedy register-then-drain ordering let a pocket callout claim strip room a
52
+ principal needed, dropping a step-height dim; post-#734 the principals place first and
53
+ all survive (#734: principals win by construction)."""
54
+ part = Box(60, 40, 60)
55
+ z, w = 60, 50
56
+ for _ in range(4): # step tower → the front-right step-height chain
57
+ part += Pos(0, 0, z + 3) * Box(w, 40, 6)
58
+ z += 6
59
+ w -= 9
60
+ zs = [6 + i * (48 / (_N_POCKETS - 1)) for i in range(_N_POCKETS)]
61
+ for i, zz in enumerate(zs): # blind pockets in the front face → competing decoration
62
+ x = -18 if i % 2 == 0 else 18
63
+ part -= Pos(x, -20, zz) * Box(9, 10, 4)
64
+ return part
65
+
66
+
67
+ @pytest.mark.timeout(120)
68
+ def test_full_front_strip_sheds_decoration_not_principal_dims():
69
+ # The #733 canary — the #734 "principals win by construction" guarantee, in the fast
70
+ # tier. Empirically calibrated: this exact part drops a step-height dim
71
+ # (placement_unsatisfiable, "front-view right strip full") on the pre-#734 commit
72
+ # 190e9ba, and is clean here on main.
73
+ dwg = build_drawing(_contended_front_strip_part())
74
+ anns = set(dwg.annotations())
75
+
76
+ # 1. No error-severity lint — directly catches the #733 `placement_unsatisfiable`
77
+ # "front-view right strip full" drop.
78
+ errors = [(i.code, i.message) for i in dwg.lint() if i.severity == "error"]
79
+ assert not errors, f"a principal dim was dropped from the full strip (#733 class): {errors}"
80
+
81
+ # 2. The principal chain is intact: overall height + the full step-height ladder, all
82
+ # on the front view. (The pre-#734 commit drops one of these — see the docstring.)
83
+ assert "dim_height" in anns, "overall-height dim dropped (front-right strip regression)"
84
+ steps = sorted(n for n in anns if n.startswith("dim_step_"))
85
+ assert len(steps) >= 5, f"step-height chain thinned out — expected >=5, got {steps}"
86
+ assert all(dwg.view_of(n) == "front" for n in ["dim_height", *steps])
87
+
88
+ # 3. Non-vacuous: the competing pocket decoration really is present, and on the SAME
89
+ # (front) view as the height chain — so guards 1–2 aren't trivially met by a part
90
+ # with no strip contention. (The decoration is not itself shed on main; the pre-#734
91
+ # ordering is what dropped a principal — see the docstring.) Fails closed: a future
92
+ # geometry change that removes the competing load would quietly turn this back into a
93
+ # smoke test, and this guard trips instead.
94
+ pockets = [n for n in anns if n.startswith("m_pocket")]
95
+ assert pockets, "no competing pocket decoration placed — the front strip is not contended"
96
+ assert all(dwg.view_of(n) == "front" for n in pockets), (
97
+ f"pocket decoration landed off the front view — not contending the height chain: "
98
+ f"{[(n, dwg.view_of(n)) for n in pockets]}"
99
+ )
@@ -15,11 +15,33 @@ from a bounded strategy so shrinking reduces real geometry.
15
15
  Budget: each example is one real OCC build (~3-6 s), so the example count is
16
16
  deliberately small and the per-example deadline is disabled. This is a bounded CI
17
17
  tier, not an exhaustive sweep — the seeded tier remains the determinism gate.
18
+
19
+ Reproducibility (#692): the tier is ``derandomize=True`` by default, so every run
20
+ generates the *same* fixed 25-example set of input parts rather than a fresh random
21
+ draw. This is the same fixed-seed philosophy as the seeded `test_layout_property.py`
22
+ tier, and it fixes the #692 defect: previously the tier drew random examples with no
23
+ persisted example database, so a failure surfaced on a seed-dependent *minority* of
24
+ runs and never replayed on the next — untrustworthy as a PR gate. Now the search is
25
+ deterministic, so within a build environment a failure recurs on every run; the input
26
+ is also reproducible from the ``@reproduce_failure`` blob ``print_blob=True`` emits
27
+ (the geometry it replays into is a pure function of that input — guarded by
28
+ ``test_layout_cleanliness`` — with fonts path-pinned per ADR 0006). Derandomising
29
+ fixes input *generation*, not the OCC build result, so a genuine layout defect may
30
+ still surface on one matrix leg (build123d/OCP versions differ across the Python legs)
31
+ before another — the pre-existing cross-config property the seeded tier shares, not
32
+ new flakiness this introduces.
33
+
34
+ Genuine exploratory fuzzing — the shrink-and-find value that once surfaced the
35
+ diameter-row shelf-flip bug — is opted into with ``DRAFTWRIGHT_FUZZ_EXPLORE=1 pytest
36
+ tests/test_layout_hypothesis.py`` (``--hypothesis-seed`` does *not* override
37
+ ``derandomize``), which draws a fresh random set and can be scheduled as a nightly
38
+ job; pin any failure it minimises as an ``@example`` here.
18
39
  """
19
40
 
20
41
  from __future__ import annotations
21
42
 
22
43
  import math
44
+ import os
23
45
 
24
46
  import pytest
25
47
  from build123d import Align, Box, Cylinder, Pos, Rotation
@@ -46,10 +68,17 @@ _DEFECTS = {
46
68
 
47
69
  # Each example is a full build; keep the count small and disable the per-example
48
70
  # deadline (OCC builds blow any millisecond budget). too_slow/filter health checks
49
- # are expected here for the same reason.
71
+ # are expected here for the same reason. `derandomize` + `print_blob` (#692) make the
72
+ # tier a reproducible PR gate: the same fixed example set every run, and any failure
73
+ # prints a paste-able `@reproduce_failure` blob (see the module docstring). Set
74
+ # DRAFTWRIGHT_FUZZ_EXPLORE=1 to draw a fresh random set instead (`--hypothesis-seed`
75
+ # does not override derandomize, so the env var is the exploration opt-in).
76
+ _EXPLORE = os.environ.get("DRAFTWRIGHT_FUZZ_EXPLORE") == "1"
50
77
  _LAYOUT_SETTINGS = settings(
51
78
  max_examples=25,
52
79
  deadline=None,
80
+ derandomize=not _EXPLORE,
81
+ print_blob=True,
53
82
  suppress_health_check=[HealthCheck.too_slow, HealthCheck.filter_too_much],
54
83
  )
55
84
 
@@ -3671,6 +3671,47 @@ def test_generate_script_preserves_pmi_scale_page(tmp_path):
3671
3671
  assert "pmi=PMI," in content and "scale=SCALE," in content and "page=PAGE," in content
3672
3672
 
3673
3673
 
3674
+ def test_generate_script_preserves_title_block_and_furniture(tmp_path):
3675
+ # #775: the imperative script must round-trip the standing title-block fields (#766) and
3676
+ # the sheet furniture (frame #767 / zones #768 / projection #769) — as config fields AND
3677
+ # threaded into the emitted build_drawing() call — so running it reproduces the CLI's
3678
+ # drawing (the sheet flavour already round-trips these; this closes the imperative gap).
3679
+ step = tmp_path / "p.step"
3680
+ export_step(Box(30, 20, 10), str(step))
3681
+ py = generate_script(
3682
+ str(step),
3683
+ out=str(tmp_path / "p"),
3684
+ material="STEEL",
3685
+ date="2026-07-20",
3686
+ revision="B",
3687
+ company="ACME",
3688
+ frame=True,
3689
+ zones=True,
3690
+ projection="third",
3691
+ )
3692
+ content = Path(py).read_text(encoding="utf-8")
3693
+ for cfg in (
3694
+ "MATERIAL = 'STEEL'",
3695
+ "DATE = '2026-07-20'",
3696
+ "REVISION = 'B'",
3697
+ "COMPANY = 'ACME'",
3698
+ "FRAME = True",
3699
+ "ZONES = True",
3700
+ "PROJECTION = 'third'",
3701
+ ):
3702
+ assert cfg in content, cfg
3703
+ for threaded in (
3704
+ "material=MATERIAL,",
3705
+ "date=DATE,",
3706
+ "revision=REVISION,",
3707
+ "company=COMPANY,",
3708
+ "frame=FRAME,",
3709
+ "zones=ZONES,",
3710
+ "projection=PROJECTION,",
3711
+ ):
3712
+ assert threaded in content, threaded
3713
+
3714
+
3674
3715
  def test_generate_script_uses_modern_dict_export(tmp_path):
3675
3716
  # #709: the emitted export is the modern {format: path} dict form (default pdf,
3676
3717
  # matching the CLI / sheet flavour) — never the deprecated legacy tuple path.
@@ -5635,6 +5676,23 @@ class TestDetailView:
5635
5676
  _request_prismatic_detail(None, a, ctx=with_escalation)
5636
5677
  assert len(with_escalation.detail_requests) == 1
5637
5678
 
5679
+ def test_unplaceable_detail_is_reported_not_silently_dropped(self):
5680
+ # #630: on a shelled cover the crowded step band is full-width (stacked face
5681
+ # levels), so a detail can't be enlarged legibly AND still fit — detail_view=True
5682
+ # used to be a silent no-op (output byte-identical to False). It must now SAY SO:
5683
+ # a `detail_unplaceable` warning that False does not carry.
5684
+ cover = (
5685
+ Box(90, 64, 38)
5686
+ - Pos(0, 0, -3) * Box(84, 58, 38)
5687
+ + Pos(0, 0, 24) * Cylinder(14, 10)
5688
+ - Pos(0, 0, 15) * Cylinder(6, 40)
5689
+ )
5690
+ off = build_drawing(cover, title="Cover", detail_view=False)
5691
+ on = build_drawing(cover, title="Cover", detail_view=True)
5692
+ assert off.lint_summary()["by_code"].get("detail_unplaceable", 0) == 0
5693
+ assert on.lint_summary()["by_code"].get("detail_unplaceable", 0) == 1
5694
+ assert "detail_a" not in on.views # genuinely couldn't place, so no empty box
5695
+
5638
5696
  def test_plain_part_gets_no_detail_view(self):
5639
5697
  dwg = build_drawing(Box(60, 40, 20))
5640
5698
  assert "detail_a" not in dwg.views
@@ -8486,6 +8544,56 @@ class TestPrismaticBossDiameter:
8486
8544
  dwg.remove(height_name)
8487
8545
  assert dwg.lint_summary()["by_code"]["boss_height_missing"] == 1
8488
8546
 
8547
+ def test_issue_631_step_on_boss_raises(self):
8548
+ # #631: reaching for .step(boss) declared a z-turned segment at the boss cylinder,
8549
+ # which flipped the height ladder into turned-suppression and silently dropped the
8550
+ # overall height (net −1 annotation, no height dim). The wrong-verb misuse must fail
8551
+ # loudly instead — .boss() is the verb for a boss. Covers both the lone .step(boss)
8552
+ # and the .boss(boss)+.step(boss) collision (both z-oriented on a prismatic body).
8553
+ from draftwright import Sheet
8554
+
8555
+ boss = Pos(0, 0, 24) * Cylinder(14, 10)
8556
+ part = Box(90, 64, 38) + boss
8557
+ for extra_boss in (False, True):
8558
+ sheet = Sheet(part, title="C")
8559
+ sheet.envelope()
8560
+ if extra_boss:
8561
+ sheet.boss(boss)
8562
+ sheet.step(boss)
8563
+ with pytest.raises(ValueError, match="don't span this part's full height"):
8564
+ sheet.build()
8565
+
8566
+ def test_issue_631_stepped_boss_on_plate_raises(self):
8567
+ # The guard keys on the exact suppression premise (do the steps span the full
8568
+ # height?), not a rotational classifier — so even a stepped boss whose two stacked
8569
+ # cylinders read as a turned PROFILE, sat on a square plate, is caught: the steps
8570
+ # cover only the boss, not the plate below, so the overall height would be dropped.
8571
+ from draftwright import Sheet
8572
+
8573
+ lower = Pos(0, 0, 25) * Cylinder(35, 10)
8574
+ upper = Pos(0, 0, 35) * Cylinder(25, 10)
8575
+ part = Box(100, 100, 20) + lower + upper
8576
+ sheet = Sheet(part, title="C")
8577
+ sheet.envelope()
8578
+ sheet.step(lower)
8579
+ sheet.step(upper)
8580
+ with pytest.raises(ValueError, match="don't span this part's full height"):
8581
+ sheet.build()
8582
+
8583
+ def test_issue_631_interior_gap_between_steps_raises(self):
8584
+ # Coverage is a union tiling, not a reach-to-each-end check: two z-steps that touch
8585
+ # both ends of a z=[0,40] body but leave a 15..25 interior gap do NOT convey the
8586
+ # full height (the gap length is unmeasured), so this must still raise.
8587
+ from draftwright.model.declare import step
8588
+
8589
+ part = Pos(0, 0, 20) * Cylinder(10, 40) # z-extent [0, 40]
8590
+ model = [
8591
+ step(diameter=20, length=15, at=(0, 0, 7.5), axis="z"), # z [0, 15]
8592
+ step(diameter=20, length=15, at=(0, 0, 32.5), axis="z"), # z [25, 40]
8593
+ ]
8594
+ with pytest.raises(ValueError, match="don't span this part's full height"):
8595
+ build_drawing(part, model=model, number="X")
8596
+
8489
8597
  def test_shelled_cover_boss_diameter_not_dropped(self):
8490
8598
  # The regression: even forced onto A4 (scale 0.5, front view against the left
8491
8599
  # margin) the boss ø28 places into the clear sheet, so it never lints uncovered.
File without changes
File without changes
File without changes
File without changes