draftwright 0.3.4__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.4 → draftwright-0.3.6}/CHANGELOG.md +83 -1
  2. {draftwright-0.3.4 → draftwright-0.3.6}/PKG-INFO +2 -2
  3. {draftwright-0.3.4 → draftwright-0.3.6}/pyproject.toml +2 -2
  4. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/_core.py +144 -0
  5. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/analysis.py +19 -1
  6. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/annotations/orchestrator.py +25 -0
  7. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/annotations/sections.py +17 -0
  8. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/builder.py +98 -2
  9. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/cli.py +25 -0
  10. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/compose.py +23 -2
  11. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/linting/structural.py +16 -1
  12. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/sheet.py +6 -0
  13. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/sheet_emit.py +15 -0
  14. draftwright-0.3.6/tests/test_dense_sheet_canary.py +99 -0
  15. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_layout_hypothesis.py +30 -1
  16. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_make_drawing.py +250 -1
  17. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_sheet_emit.py +43 -0
  18. {draftwright-0.3.4 → draftwright-0.3.6}/.gitignore +0 -0
  19. {draftwright-0.3.4 → draftwright-0.3.6}/LICENSE +0 -0
  20. {draftwright-0.3.4 → draftwright-0.3.6}/README.md +0 -0
  21. {draftwright-0.3.4 → draftwright-0.3.6}/docs/adr/README.md +0 -0
  22. {draftwright-0.3.4 → draftwright-0.3.6}/skills/SKILL.md +0 -0
  23. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/__init__.py +0 -0
  24. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/_geometry.py +0 -0
  25. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/annotate.py +0 -0
  26. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/annotations/__init__.py +0 -0
  27. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/annotations/_common.py +0 -0
  28. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/annotations/balloons.py +0 -0
  29. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/annotations/from_model.py +0 -0
  30. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/annotations/holes.py +0 -0
  31. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/drawing.py +0 -0
  32. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/export.py +0 -0
  33. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/fits.py +0 -0
  34. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
  35. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
  36. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
  37. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
  38. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/fonts/__init__.py +0 -0
  39. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/intents.py +0 -0
  40. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/layout.py +0 -0
  41. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/linting/__init__.py +0 -0
  42. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/linting/coverage.py +0 -0
  43. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/linting/issues.py +0 -0
  44. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/linting/suggest.py +0 -0
  45. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/make_drawing.py +0 -0
  46. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/model/__init__.py +0 -0
  47. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/model/declare.py +0 -0
  48. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/model/detect.py +0 -0
  49. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/model/ir.py +0 -0
  50. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/model/planner.py +0 -0
  51. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/pmi.py +0 -0
  52. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/projection.py +0 -0
  53. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/recognition/__init__.py +0 -0
  54. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/recognition/_features.py +0 -0
  55. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/recognition/_record.py +0 -0
  56. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/recognition/chamfers.py +0 -0
  57. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/recognition/countersinks.py +0 -0
  58. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/recognition/fillets.py +0 -0
  59. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/recognition/flats.py +0 -0
  60. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/recognition/grooves.py +0 -0
  61. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/recognition/levels.py +0 -0
  62. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/recognition/plates.py +0 -0
  63. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/recognition/slots.py +0 -0
  64. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/recognition/turned.py +0 -0
  65. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/registry.py +0 -0
  66. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/repair.py +0 -0
  67. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/score.py +0 -0
  68. {draftwright-0.3.4 → draftwright-0.3.6}/src/draftwright/sheet_dsl.py +0 -0
  69. {draftwright-0.3.4 → draftwright-0.3.6}/tests/_kernel.py +0 -0
  70. {draftwright-0.3.4 → draftwright-0.3.6}/tests/_layout_sig.py +0 -0
  71. {draftwright-0.3.4 → draftwright-0.3.6}/tests/fixtures/grm03_thumbwheel_drive_screw.step +0 -0
  72. {draftwright-0.3.4 → draftwright-0.3.6}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
  73. {draftwright-0.3.4 → draftwright-0.3.6}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
  74. {draftwright-0.3.4 → draftwright-0.3.6}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
  75. {draftwright-0.3.4 → draftwright-0.3.6}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
  76. {draftwright-0.3.4 → draftwright-0.3.6}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
  77. {draftwright-0.3.4 → draftwright-0.3.6}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
  78. {draftwright-0.3.4 → draftwright-0.3.6}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
  79. {draftwright-0.3.4 → draftwright-0.3.6}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
  80. {draftwright-0.3.4 → draftwright-0.3.6}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
  81. {draftwright-0.3.4 → draftwright-0.3.6}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
  82. {draftwright-0.3.4 → draftwright-0.3.6}/tests/refactor_golden/bracket_section.json +0 -0
  83. {draftwright-0.3.4 → draftwright-0.3.6}/tests/refactor_golden/centered_rebate.json +0 -0
  84. {draftwright-0.3.4 → draftwright-0.3.6}/tests/refactor_golden/chamfered.json +0 -0
  85. {draftwright-0.3.4 → draftwright-0.3.6}/tests/refactor_golden/filleted.json +0 -0
  86. {draftwright-0.3.4 → draftwright-0.3.6}/tests/refactor_golden/flange_dense.json +0 -0
  87. {draftwright-0.3.4 → draftwright-0.3.6}/tests/refactor_golden/grid_plate.json +0 -0
  88. {draftwright-0.3.4 → draftwright-0.3.6}/tests/refactor_golden/grooved_shaft.json +0 -0
  89. {draftwright-0.3.4 → draftwright-0.3.6}/tests/refactor_golden/hex_bar.json +0 -0
  90. {draftwright-0.3.4 → draftwright-0.3.6}/tests/refactor_golden/holed_slot.json +0 -0
  91. {draftwright-0.3.4 → draftwright-0.3.6}/tests/refactor_golden/pocketed.json +0 -0
  92. {draftwright-0.3.4 → draftwright-0.3.6}/tests/refactor_golden/prismatic_ladder.json +0 -0
  93. {draftwright-0.3.4 → draftwright-0.3.6}/tests/refactor_golden/scattered_plate.json +0 -0
  94. {draftwright-0.3.4 → draftwright-0.3.6}/tests/refactor_golden/side_drilled.json +0 -0
  95. {draftwright-0.3.4 → draftwright-0.3.6}/tests/refactor_golden/turned_stepped.json +0 -0
  96. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_architecture_docs.py +0 -0
  97. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_carve_free_position_callers.py +0 -0
  98. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_declare.py +0 -0
  99. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_detect_once.py +0 -0
  100. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_drawing_encapsulation.py +0 -0
  101. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_e2e_slice.py +0 -0
  102. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_e2e_standards.py +0 -0
  103. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_export_dxf_zoom.py +0 -0
  104. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_fillets_adjacency.py +0 -0
  105. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_fits.py +0 -0
  106. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_gdt_placement.py +0 -0
  107. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_import_boundaries.py +0 -0
  108. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_layout.py +0 -0
  109. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_layout_cleanliness.py +0 -0
  110. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_layout_property.py +0 -0
  111. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_lint_box_cache.py +0 -0
  112. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_lint_reconciliation.py +0 -0
  113. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_lint_structural.py +0 -0
  114. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_linting.py +0 -0
  115. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_object_aspects.py +0 -0
  116. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_occupancy_boxes.py +0 -0
  117. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_part_model.py +0 -0
  118. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_pitch_dim_footprint.py +0 -0
  119. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_pmi.py +0 -0
  120. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_private_test_imports.py +0 -0
  121. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_recogniser_contract.py +0 -0
  122. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_recognition.py +0 -0
  123. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_refactor_golden.py +0 -0
  124. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_registry.py +0 -0
  125. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_render_seam.py +0 -0
  126. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_score.py +0 -0
  127. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_script_detail_parity.py +0 -0
  128. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_sheet_gdt.py +0 -0
  129. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_sheet_notes.py +0 -0
  130. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_sheet_of.py +0 -0
  131. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_sheet_tables.py +0 -0
  132. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_solve_trace.py +0 -0
  133. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_strip_layout.py +0 -0
  134. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_tolerances.py +0 -0
  135. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_turned_steps.py +0 -0
  136. {draftwright-0.3.4 → draftwright-0.3.6}/tests/test_witness_label_reconciliation.py +0 -0
@@ -1,6 +1,88 @@
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`.
47
+
48
+ ## v0.3.5 — 2026-07-20
49
+
50
+ ### Fixed
51
+
52
+ - **Generated `Sheet` script round-trips `--frame` / `--zones` / `--projection`**:
53
+ the sheet-furniture flags added in #767/#768/#769 were not emitted into the
54
+ generated `Sheet(...)` constructor (nor forwarded by the CLI's `--script` path),
55
+ so a regenerated script dropped the border / zone ruler / projection symbol —
56
+ the script no longer matched the direct CLI drawing. The emitter now emits
57
+ `frame=True` / `zones=True` / `projection=…` when set (a plain drawing keeps a
58
+ clean constructor), and the CLI forwards them to `generate_sheet_script`.
59
+
60
+ ### Added
61
+
62
+ - **ISO 5457 zone-grid border ruler** (#768): `build_drawing(zones=True)` /
63
+ `Sheet(zones=True)` / `--zones` draws the grid reference system — numbers 1..
64
+ along the top/bottom edges, letters A.. (skipping I/O) down the sides — in the
65
+ band between the frame and the page edge, with the standard division count per
66
+ A-series page (~50 mm zones). Implies a frame (the ticks sit on it). Off by
67
+ default → byte-identical. The border ticks are lint-exempt like the frame; the
68
+ labels are exempt only from the page-bounds check (they legitimately sit outside
69
+ the drawable), still covered by overlap lint.
70
+ - **Projection-method symbol** (#769): `build_drawing(projection="third"|"first")` /
71
+ `Sheet(projection=…)` / `--projection` draws the ISO 5456-2 third-/first-angle
72
+ glyph in the reserved title-block band (above the title block). Uses the new
73
+ `ProjectionSymbol` primitive from build123d-drafting-helpers **0.14.1** (the pin
74
+ is bumped). Off by default → byte-identical. Unlike the page-spanning frame, the
75
+ small well-placed glyph is *not* lint-exempt — lint covers it so a future
76
+ mispositioning is caught.
77
+ - **Opt-in drawn sheet frame / border** (#767): `build_drawing(frame=True)` /
78
+ `Sheet(frame=True)` / `--frame` draws a border rectangle at the page margin. It's
79
+ the *content boundary* (Option B), not a rectangle over the drawing: turning it on
80
+ raises the effective content margin, and because `compose._layout_geometry` is the
81
+ single authority shared by scale/page selection **and** placement, the reservation
82
+ flows through `choose_scale` — so a framed drawing may pick a smaller scale / larger
83
+ page (ADR 0004), with content clearing the border. Default `frame=False` is
84
+ byte-identical (guarded by the golden + layout-cleanliness suites). The page-spanning
85
+ border carries an `is_sheet_frame` rider so lint skips it.
4
86
 
5
87
  ## v0.3.4 — 2026-07-20
6
88
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: draftwright
3
- Version: 0.3.4
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
@@ -682,7 +682,7 @@ Classifier: Programming Language :: Python :: 3.13
682
682
  Classifier: Programming Language :: Python :: 3.14
683
683
  Classifier: Topic :: Scientific/Engineering
684
684
  Requires-Python: <3.15,>=3.10
685
- Requires-Dist: build123d-drafting-helpers>=0.14.0
685
+ Requires-Dist: build123d-drafting-helpers>=0.14.1
686
686
  Requires-Dist: build123d<0.11,>=0.9.0; python_full_version < '3.13'
687
687
  Requires-Dist: build123d<0.12,>=0.11; python_full_version >= '3.13'
688
688
  Requires-Dist: numpy>=1.24
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "draftwright"
7
- version = "0.3.4"
7
+ version = "0.3.6"
8
8
  description = "Automated technical-drawing generation for build123d"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -17,7 +17,7 @@ dependencies = [
17
17
  # 3.13+. Mirrors pzfreo/build123d-mcp's dependency handling.
18
18
  "build123d>=0.9.0,<0.11 ; python_full_version < '3.13'",
19
19
  "build123d>=0.11,<0.12 ; python_full_version >= '3.13'",
20
- "build123d-drafting-helpers>=0.14.0",
20
+ "build123d-drafting-helpers>=0.14.1",
21
21
  "numpy>=1.24",
22
22
  # PNG raster export (SVG→PDF→PNG). Both permissively licensed (Pillow HPND; pypdfium2
23
23
  # Apache-2.0 wrapping Google PDFium BSD-3) and ship pre-built wheels with NO native system
@@ -50,6 +50,44 @@ _log = logging.getLogger(__name__)
50
50
 
51
51
  _MARGIN = 10.0
52
52
 
53
+ # When a sheet frame is drawn (#767), content reserves this extra band inside the border so
54
+ # it clears the drawn line rather than sitting on it. The frame draws AT _MARGIN (the old
55
+ # drawable boundary); content insets to _content_margin(frame).
56
+ _FRAME_BAND = 6.0
57
+
58
+
59
+ def _content_margin(frame: bool) -> float:
60
+ """The effective content margin: ``_MARGIN``, plus the frame clearance band when a sheet
61
+ frame is drawn (#767). Threaded into the layout authority so the reservation flows through
62
+ scale/page selection (ADR 0004), not just the render."""
63
+ return _MARGIN + (_FRAME_BAND if frame else 0.0)
64
+
65
+
66
+ # ISO 5457 zone-grid letters (vertical edges): A.. skipping I and O (confusable with 1 / 0).
67
+ _ZONE_LETTERS = "ABCDEFGHJKLMNPQRSTUVWXYZ"
68
+
69
+ # ISO 5457 reference-grid division counts (columns=numbers × rows=letters) per A-series page,
70
+ # keyed by the full (width, height) so a same-width custom page doesn't borrow the count.
71
+ _ZONE_DIVISIONS = {
72
+ (297, 210): (6, 4),
73
+ (420, 297): (8, 6),
74
+ (594, 420): (12, 8),
75
+ (841, 594): (16, 12),
76
+ (1189, 841): (24, 16),
77
+ }
78
+
79
+
80
+ def _zone_divisions(page_w: float, page_h: float) -> tuple[int, int]:
81
+ """``(columns, rows)`` for the ISO 5457 zone grid (#768) — the standard count for an
82
+ A-series page (matched on BOTH dimensions), else ~50 mm zones for a custom page. Rows are
83
+ clamped to the available letters (I/O skipped) so a very tall custom page can't over-index."""
84
+ std = _ZONE_DIVISIONS.get((int(round(page_w)), int(round(page_h))))
85
+ if std is not None:
86
+ return std
87
+ cols = max(2, round(page_w / 49.5))
88
+ rows = min(len(_ZONE_LETTERS), max(2, round(page_h / 49.5)))
89
+ return (cols, rows)
90
+
53
91
 
54
92
  _TB_CLEAR = _MARGIN + 1.0 # title-block inset: one extra mm over _MARGIN for clearance
55
93
 
@@ -711,6 +749,13 @@ class Analysis:
711
749
  date: str = ""
712
750
  revision: str = "A"
713
751
  company: str = ""
752
+ # Draw a sheet border/frame (#767). When True, `margin` is already the reserved content
753
+ # margin (`_content_margin(True)`), so content clears the frame drawn at `_MARGIN`.
754
+ frame: bool = False
755
+ # Projection-method symbol (#769): "third" / "first" (ISO 5456-2) or None (omit).
756
+ projection: str | None = None
757
+ # Draw the ISO 5457 zone-grid border ruler (#768). Implies a frame (the ticks sit on it).
758
+ zones: bool = False
714
759
  # The PartModel built by _analyse's pre-scale sizing pass (#584 WP1 A) — stored so
715
760
  # the render path reuses it instead of re-running the detectors (ADR 0008 Amdt 5:
716
761
  # one inventory, detected once; #602). Typed `object` to keep _core free of a
@@ -799,6 +844,105 @@ def _add_title_block(dwg, a: Analysis):
799
844
  dwg.add(tb, "title_block")
800
845
 
801
846
 
847
+ def _make_sheet_frame(a: Analysis) -> Compound:
848
+ """The sheet border rectangle (#767) — a closed outline at the ``_MARGIN`` inset (the old
849
+ drawable boundary). Content clears it because ``a.margin`` is the reserved content margin.
850
+ Carries an ``is_sheet_frame`` rider (like ``is_centerline``) so lint skips its page-spanning
851
+ box, and so ``get_annotation`` / a removed frame drop it cleanly."""
852
+ x0, y0 = _MARGIN, _MARGIN
853
+ x1, y1 = a.PAGE_W - _MARGIN, a.PAGE_H - _MARGIN
854
+ frame = Compound(
855
+ children=[
856
+ Edge.make_line(Vector(x0, y0, 0), Vector(x1, y0, 0)),
857
+ Edge.make_line(Vector(x1, y0, 0), Vector(x1, y1, 0)),
858
+ Edge.make_line(Vector(x1, y1, 0), Vector(x0, y1, 0)),
859
+ Edge.make_line(Vector(x0, y1, 0), Vector(x0, y0, 0)),
860
+ ]
861
+ )
862
+ frame.is_sheet_frame = True # furniture, not a dimension/view — exempt from overlap lint
863
+ return frame
864
+
865
+
866
+ def _add_sheet_frame(dwg, a: Analysis):
867
+ """Add the sheet border (#767), drawn last like the title block. No-op is the caller's
868
+ (gated on ``a.frame``)."""
869
+ dwg.add(_make_sheet_frame(a), "sheet_frame")
870
+
871
+
872
+ def _add_projection_symbol(dwg, a: Analysis):
873
+ """Place the ISO 5456-2 projection-method glyph (#769) in the reserved title-block band,
874
+ just above the drawn title block (deterministic empty space — the block reserves _TB_H but
875
+ draws shorter). Registered ``projection_symbol`` with an ``is_projection_symbol`` identity
876
+ rider. Unlike the page-spanning frame it is NOT lint-exempt: it's a small, well-placed glyph,
877
+ so lint covers it and a future mispositioning is caught. Gated on ``a.projection``."""
878
+ from build123d_drafting import ProjectionSymbol
879
+
880
+ sym = ProjectionSymbol(
881
+ a.projection,
882
+ draft=draft_preset(
883
+ font_size=dwg.draft.font_size,
884
+ decimal_precision=dwg.draft.decimal_precision,
885
+ font_path=PLEX_SANS_CONDENSED,
886
+ ),
887
+ )
888
+ b = sym.bounding_box()
889
+ bx, by = (b.min.X + b.max.X) / 2, (b.min.Y + b.max.Y) / 2
890
+ w, h = b.max.X - b.min.X, b.max.Y - b.min.Y
891
+ # Right side of the title-block column, near the top of its reserved band.
892
+ cx = a.PAGE_W - _TB_CLEAR - w / 2 - 3
893
+ cy = _TB_CLEAR + _TB_H - h / 2 - 2
894
+ sym = sym.locate(Location((cx - bx, cy - by, 0)))
895
+ sym.is_projection_symbol = True
896
+ dwg.add(sym, "projection_symbol")
897
+
898
+
899
+ def _add_zone_grid(dwg, a: Analysis):
900
+ """Draw the ISO 5457 zone-grid border ruler (#768) — numbers 1.. along the top/bottom
901
+ edges, letters A.. (skipping I/O) down the left/right — in the band between the frame
902
+ (at ``_MARGIN``) and the page edge. Requires ``a.frame`` (the ticks sit on the border);
903
+ the caller gates on ``a.zones`` and ensures the frame. Tick lines register as ``zone_grid``
904
+ (``is_zone_grid``); each label carries an ``is_zone_label`` rider so it is exempt from the
905
+ page-bounds lint (it legitimately sits outside the drawable) — but NOT from overlap lint."""
906
+ from build123d_drafting import Note
907
+
908
+ cols, rows = _zone_divisions(a.PAGE_W, a.PAGE_H)
909
+ x0, y0, x1, y1 = _MARGIN, _MARGIN, a.PAGE_W - _MARGIN, a.PAGE_H - _MARGIN
910
+ cw, rh = (x1 - x0) / cols, (y1 - y0) / rows
911
+ band = _MARGIN
912
+ tick = min(3.0, band * 0.6)
913
+ draft = draft_preset(
914
+ font_size=dwg.draft.font_size * 0.8,
915
+ decimal_precision=dwg.draft.decimal_precision,
916
+ font_path=PLEX_SANS_CONDENSED,
917
+ )
918
+ ticks = []
919
+ for i in range(1, cols): # interior column boundaries → ticks on top + bottom edges
920
+ xb = x0 + i * cw
921
+ ticks.append(Edge.make_line(Vector(xb, y0, 0), Vector(xb, y0 - tick, 0)))
922
+ ticks.append(Edge.make_line(Vector(xb, y1, 0), Vector(xb, y1 + tick, 0)))
923
+ for j in range(1, rows): # interior row boundaries → ticks on left + right edges
924
+ yb = y0 + j * rh
925
+ ticks.append(Edge.make_line(Vector(x0, yb, 0), Vector(x0 - tick, yb, 0)))
926
+ ticks.append(Edge.make_line(Vector(x1, yb, 0), Vector(x1 + tick, yb, 0)))
927
+ grid = Compound(children=ticks)
928
+ grid.is_zone_grid = True
929
+ dwg.add(grid, "zone_grid")
930
+
931
+ def _label(text, cx, cy, name):
932
+ note = Note(text, (cx, cy), draft, align=(Align.CENTER, Align.CENTER))
933
+ note.is_zone_label = True
934
+ dwg.add(note, name)
935
+
936
+ for i in range(cols): # numbers 1.. left→right, in the bottom + top bands
937
+ cx = x0 + (i + 0.5) * cw
938
+ _label(str(i + 1), cx, y0 - band / 2, f"zone_num_b_{i}")
939
+ _label(str(i + 1), cx, y1 + band / 2, f"zone_num_t_{i}")
940
+ for j in range(rows): # letters A.. top→bottom, in the left + right bands
941
+ cy = y1 - (j + 0.5) * rh
942
+ _label(_ZONE_LETTERS[j], x0 - band / 2, cy, f"zone_ltr_l_{j}")
943
+ _label(_ZONE_LETTERS[j], x1 + band / 2, cy, f"zone_ltr_r_{j}")
944
+
945
+
802
946
  def _iso_bbox(dwg):
803
947
  """(min_x, min_y, max_x, max_y) of the placed iso view, hidden lines included."""
804
948
  return dwg.view_bounds("iso")
@@ -29,6 +29,7 @@ from draftwright._core import (
29
29
  _MIN_RENDER_MM,
30
30
  _MIN_VIEW_MM,
31
31
  Analysis,
32
+ _content_margin,
32
33
  _legible_steps,
33
34
  _Projector,
34
35
  )
@@ -409,6 +410,7 @@ def _validate_explicit_scale(
409
410
  strips_i,
410
411
  layout_section,
411
412
  layout_table_sizes,
413
+ margin=_MARGIN,
412
414
  ) -> None:
413
415
  """Enforce the two scale floors when the caller pinned an explicit *scale* (#489, #590 split
414
416
  of :func:`_analyse`). An explicit scale is the user's call — honour it, subject to:
@@ -441,6 +443,7 @@ def _validate_explicit_scale(
441
443
  strips=strips_i,
442
444
  section=layout_section,
443
445
  table_sizes=layout_table_sizes,
446
+ margin=margin,
444
447
  )
445
448
  # Warn only when omitting the scale would truly give a legible fit (auto scale itself is
446
449
  # legible) but the requested scale is below the floor. A part illegible at every
@@ -474,11 +477,20 @@ def _analyse(
474
477
  date="",
475
478
  revision="A",
476
479
  company="",
480
+ frame: bool = False,
481
+ projection: str | None = None,
482
+ zones: bool = False,
477
483
  ) -> Analysis:
478
484
  """Load STEP or use a build123d Shape, analyse geometry, compute layout.
479
485
 
480
486
  Returns an :class:`Analysis`.
481
487
  """
488
+ # The zone-grid ruler (#768) draws its ticks on the frame, so it implies one.
489
+ frame = frame or zones
490
+ # The content margin — raised by the sheet-frame band (#767) so scale/page selection and
491
+ # placement both reserve room for the border. Computed up front so the choose_scale inside
492
+ # step-count convergence sees it too.
493
+ margin = _content_margin(frame)
482
494
  if isinstance(step_file, Shape):
483
495
  part = step_file
484
496
  src = "build123d object"
@@ -609,6 +621,7 @@ def _analyse(
609
621
  strips=strips_i,
610
622
  section=layout_section,
611
623
  table_sizes=layout_table_sizes,
624
+ margin=margin,
612
625
  )
613
626
 
614
627
  (SCALE, PAGE_W, PAGE_H, TB_W), strips_i, n_for_sizing = _converge_step_sizing(
@@ -628,9 +641,10 @@ def _analyse(
628
641
  strips_i,
629
642
  layout_section,
630
643
  layout_table_sizes,
644
+ margin=margin,
631
645
  )
632
646
  DIM_PAD = _DIM_PAD
633
- margin = _MARGIN
647
+ # margin was computed up front (_content_margin(frame)) so scale selection already saw it.
634
648
  # Refine: apply the same legibility gate _auto_annotate uses for dim_step.
635
649
  n_steps = len(_legible_steps(step_zs, bb.min.Z, SCALE)[0])
636
650
  strips = _measure_strips(
@@ -656,6 +670,7 @@ def _analyse(
656
670
  n_steps,
657
671
  section=layout_section,
658
672
  table_sizes=layout_table_sizes,
673
+ margin=margin,
659
674
  )
660
675
  fv_hw = _g.fv_hw
661
676
  fv_hh = _g.fv_hh
@@ -778,6 +793,9 @@ def _analyse(
778
793
  date=date,
779
794
  revision=revision,
780
795
  company=company,
796
+ frame=frame,
797
+ projection=projection,
798
+ zones=zones,
781
799
  out=out,
782
800
  pmi=pmi_records,
783
801
  pmi_mode=pmi,
@@ -21,7 +21,10 @@ from draftwright._core import (
21
21
  _TABULATE_MIN_HOLES,
22
22
  Analysis,
23
23
  HoleRef,
24
+ _add_projection_symbol,
25
+ _add_sheet_frame,
24
26
  _add_title_block,
27
+ _add_zone_grid,
25
28
  _concentric_with_axis,
26
29
  _fmt,
27
30
  _iso_bbox,
@@ -125,6 +128,9 @@ _PASS_SEQUENCE: tuple[str, ...] = (
125
128
  "details",
126
129
  "title_block",
127
130
  "tabulate",
131
+ "sheet_frame",
132
+ "zone_grid",
133
+ "projection_symbol",
128
134
  )
129
135
 
130
136
 
@@ -508,6 +514,22 @@ def _auto_annotate(dwg, a: Analysis, *, detail_view: bool = False):
508
514
  def _s_title_block():
509
515
  _add_title_block(dwg, a)
510
516
 
517
+ def _s_sheet_frame():
518
+ # The sheet border, drawn LAST (#767) — gated on the frame opt-in; content already
519
+ # reserved room via the raised a.margin, so this only draws.
520
+ if a.frame:
521
+ _add_sheet_frame(dwg, a)
522
+
523
+ def _s_zone_grid():
524
+ # ISO 5457 zone-grid border ruler (#768), on the frame (a.zones implies a.frame).
525
+ if a.zones:
526
+ _add_zone_grid(dwg, a)
527
+
528
+ def _s_projection_symbol():
529
+ # ISO 5456-2 projection-method glyph (#769) in the reserved title-block band.
530
+ if a.projection:
531
+ _add_projection_symbol(dwg, a)
532
+
511
533
  def _s_tabulate():
512
534
  # Escalate to a hole table when the plan view is too dense to dimension
513
535
  # every hole — runs last so the table avoids every placed annotation
@@ -545,6 +567,9 @@ def _auto_annotate(dwg, a: Analysis, *, detail_view: bool = False):
545
567
  "details": _s_details,
546
568
  "title_block": _s_title_block,
547
569
  "tabulate": _s_tabulate,
570
+ "sheet_frame": _s_sheet_frame,
571
+ "zone_grid": _s_zone_grid,
572
+ "projection_symbol": _s_projection_symbol,
548
573
  }
549
574
  )
550
575
  if ctx.trace is not None: # snapshot the run's escalations into the trace (#736)
@@ -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:
@@ -29,7 +29,10 @@ from draftwright._core import (
29
29
  _PAGE_SIZES,
30
30
  _SCALES,
31
31
  Analysis,
32
+ _add_projection_symbol,
33
+ _add_sheet_frame,
32
34
  _add_title_block,
35
+ _add_zone_grid,
33
36
  _iso_bbox,
34
37
  _log,
35
38
  _parse_page,
@@ -284,6 +287,33 @@ def _assemble(
284
287
  rot = build_rotational_feature(a)
285
288
  if rot is not None:
286
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
+ )
287
317
  # PMI (STEP AP242) is likewise detection-sourced, so a declared / emitted-script model
288
318
  # carries none. When PMI annotation is on, synthesise the same imported drafting
289
319
  # annotations detection would (render_pmi reads them off the model, gated on a.pmi_mode)
@@ -336,6 +366,12 @@ def _assemble(
336
366
  else:
337
367
  _fit_iso_view(dwg, a, annotate=False)
338
368
  _add_title_block(dwg, a)
369
+ if a.frame: # sheet border, drawn last (#767) — auto path adds it via the orchestrator
370
+ _add_sheet_frame(dwg, a)
371
+ if a.zones: # zone-grid ruler (#768), on the frame
372
+ _add_zone_grid(dwg, a)
373
+ if a.projection: # projection-method glyph (#769) — auto path adds it via the orchestrator
374
+ _add_projection_symbol(dwg, a)
339
375
  return dwg
340
376
 
341
377
 
@@ -409,6 +445,7 @@ def _repack(
409
445
  section=a.layout_section,
410
446
  table_sizes=a.layout_table_sizes,
411
447
  warn_no_iso=False,
448
+ margin=a.margin,
412
449
  )
413
450
 
414
451
  candidates = _repack_candidates(a, scale, page)
@@ -603,6 +640,9 @@ def build_drawing(
603
640
  date: str = "",
604
641
  revision: str = "A",
605
642
  company: str = "",
643
+ frame: bool = False,
644
+ projection: str | None = None,
645
+ zones: bool = False,
606
646
  ) -> Drawing:
607
647
  """Build a customisable 4-view :class:`Drawing` without exporting it.
608
648
 
@@ -690,6 +730,9 @@ def build_drawing(
690
730
  date=date,
691
731
  revision=revision,
692
732
  company=company,
733
+ frame=frame,
734
+ projection=projection,
735
+ zones=zones,
693
736
  )
694
737
 
695
738
  # Pass 1: place + annotate from the estimated layout, then measure the real
@@ -754,6 +797,9 @@ def make_drawing(
754
797
  date: str = "",
755
798
  revision: str = "A",
756
799
  company: str = "",
800
+ frame: bool = False,
801
+ projection: str | None = None,
802
+ zones: bool = False,
757
803
  ) -> tuple[str, str]:
758
804
  """Generate a 4-view technical drawing from a STEP file or build123d object.
759
805
 
@@ -799,6 +845,9 @@ def make_drawing(
799
845
  date=date,
800
846
  revision=revision,
801
847
  company=company,
848
+ frame=frame,
849
+ projection=projection,
850
+ zones=zones,
802
851
  ).export()
803
852
  assert svg_path is not None and dxf_path is not None # export() writes both by default
804
853
  return svg_path, dxf_path
@@ -953,6 +1002,13 @@ def _write_script(
953
1002
  f"NUMBER = {a.number!r}",
954
1003
  f"TOLERANCE = {a.tolerance!r}",
955
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}",
956
1012
  f"PMI = {a.pmi_mode!r}",
957
1013
  f"SCALE = {scale!r}",
958
1014
  f"PAGE = {page!r}",
@@ -967,6 +1023,13 @@ def _write_script(
967
1023
  f"_NUMBER = {a.number!r}\n"
968
1024
  f"_TOLERANCE = {a.tolerance!r}\n"
969
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"
970
1033
  f"_PMI = {a.pmi_mode!r} # 'off' | 'report' | 'annotate'\n"
971
1034
  f"_SCALE = {scale!r} # None = auto; e.g. 5 for 5:1, 0.5 for 1:2\n"
972
1035
  f"_PAGE = {page!r} # None = auto; e.g. 'A3' or (297, 210)\n"
@@ -975,7 +1038,11 @@ def _write_script(
975
1038
  " for _k, _v in [\n"
976
1039
  " ('STEP_FILE', repr(_STEP_FILE)), ('TITLE', repr(_TITLE)),\n"
977
1040
  " ('NUMBER', repr(_NUMBER)), ('TOLERANCE', repr(_TOLERANCE)),\n"
978
- " ('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"
979
1046
  " ('SCALE', repr(_SCALE)), ('PAGE', repr(_PAGE)),\n"
980
1047
  " ]:\n"
981
1048
  " cog.outl(f'{_k} = {_v}')\n"
@@ -1020,6 +1087,13 @@ def _write_script(
1020
1087
  " number=NUMBER,\n"
1021
1088
  " tolerance=TOLERANCE,\n"
1022
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"
1023
1097
  " pmi=PMI,\n"
1024
1098
  " scale=SCALE,\n"
1025
1099
  " page=PAGE,\n"
@@ -1081,6 +1155,13 @@ def generate_script(
1081
1155
  pmi: Literal["off", "report", "annotate"] = "off",
1082
1156
  scale: float | None = None,
1083
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,
1084
1165
  formats: Sequence[str] = ("pdf",),
1085
1166
  ) -> str:
1086
1167
  """Generate an editable Cog-enabled drawing script from a STEP file.
@@ -1109,7 +1190,22 @@ def generate_script(
1109
1190
  # here too would crash generation on an out-of-range value (e.g. --script --scale
1110
1191
  # 0.001 / --page A9) instead of writing the script and deferring — inconsistent with
1111
1192
  # a large unfittable scale, which already defers (review #401).
1112
- 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
+ )
1113
1209
  return _write_script(a, scale=scale, page=page, formats=formats)
1114
1210
 
1115
1211