draftwright 0.4.2__tar.gz → 0.4.4__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 (227) hide show
  1. {draftwright-0.4.2 → draftwright-0.4.4}/CHANGELOG.md +136 -0
  2. {draftwright-0.4.2 → draftwright-0.4.4}/PKG-INFO +42 -6
  3. {draftwright-0.4.2 → draftwright-0.4.4}/README.md +40 -4
  4. {draftwright-0.4.2 → draftwright-0.4.4}/docs/adr/README.md +3 -2
  5. {draftwright-0.4.2 → draftwright-0.4.4}/pyproject.toml +1 -1
  6. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/__init__.py +6 -2
  7. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/_core.py +17 -0
  8. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/_geometry.py +99 -9
  9. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/_warnings.py +8 -0
  10. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/analysis.py +84 -42
  11. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/annotations/_common.py +445 -6
  12. draftwright-0.4.4/src/draftwright/annotations/balloons.py +1128 -0
  13. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/annotations/from_model.py +181 -41
  14. draftwright-0.4.4/src/draftwright/annotations/gears.py +72 -0
  15. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/annotations/holes.py +220 -89
  16. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/annotations/orchestrator.py +443 -67
  17. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/audit.py +16 -17
  18. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/builder.py +404 -6
  19. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/cli.py +20 -0
  20. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/compose.py +38 -8
  21. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/drawing.py +270 -42
  22. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/layout.py +294 -27
  23. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/linting/__init__.py +6 -0
  24. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/linting/coverage.py +23 -11
  25. draftwright-0.4.4/src/draftwright/linting/gear_coverage.py +185 -0
  26. draftwright-0.4.4/src/draftwright/linting/hole_coverage.py +1008 -0
  27. draftwright-0.4.4/src/draftwright/linting/issues.py +89 -0
  28. draftwright-0.4.4/src/draftwright/linting/polygonal_stock_coverage.py +114 -0
  29. draftwright-0.4.4/src/draftwright/linting/quality.py +352 -0
  30. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/linting/slot_coverage.py +29 -1
  31. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/linting/structural.py +120 -40
  32. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/model/__init__.py +8 -0
  33. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/model/callout.py +46 -10
  34. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/model/compiled.py +37 -11
  35. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/model/declare.py +59 -0
  36. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/model/detect.py +39 -18
  37. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/model/ir.py +173 -0
  38. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/model/planner.py +22 -40
  39. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/recognition/__init__.py +21 -2
  40. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/recognition/_features.py +2 -17
  41. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/recognition/countersinks.py +47 -0
  42. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/recognition/polygonal_bosses.py +80 -10
  43. draftwright-0.4.4/src/draftwright/recognition/repeating_profiles.py +393 -0
  44. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/recognition/result.py +13 -1
  45. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/recognition/slots.py +48 -2
  46. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/registry.py +16 -2
  47. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/sheet.py +65 -26
  48. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/sheet_emit.py +64 -0
  49. {draftwright-0.4.2 → draftwright-0.4.4}/tests/refactor_golden/bracket_section.json +2 -2
  50. {draftwright-0.4.2 → draftwright-0.4.4}/tests/refactor_golden/flange_dense.json +1 -1
  51. {draftwright-0.4.2 → draftwright-0.4.4}/tests/refactor_golden/grid_plate.json +9 -3
  52. {draftwright-0.4.2 → draftwright-0.4.4}/tests/refactor_golden/holed_slot.json +1 -1
  53. {draftwright-0.4.2 → draftwright-0.4.4}/tests/refactor_golden/pocketed.json +1 -1
  54. {draftwright-0.4.2 → draftwright-0.4.4}/tests/refactor_golden/scattered_plate.json +4 -4
  55. {draftwright-0.4.2 → draftwright-0.4.4}/tests/refactor_golden/side_drilled.json +1 -1
  56. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_add_dimension.py +21 -0
  57. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_audit_differential.py +20 -32
  58. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_compiled_plan_boundary.py +1 -0
  59. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_declare.py +8 -8
  60. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_e2e_standards.py +48 -59
  61. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_flat_completeness.py +33 -1
  62. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_gdt_placement.py +7 -1
  63. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_issue_1058_wheel_profile.py +167 -136
  64. draftwright-0.4.4/tests/test_issue_1073_cross_body_patterns.py +95 -0
  65. draftwright-0.4.4/tests/test_issue_1082_polygonal_stock.py +232 -0
  66. draftwright-0.4.4/tests/test_issue_1086_declared_gears.py +291 -0
  67. draftwright-0.4.4/tests/test_issue_1142_hole_leader_labels.py +247 -0
  68. draftwright-0.4.4/tests/test_issue_1143_hole_completeness.py +2716 -0
  69. draftwright-0.4.4/tests/test_issue_1144_transactional_hole_table.py +1774 -0
  70. draftwright-0.4.4/tests/test_issue_1146_scale_completeness.py +539 -0
  71. draftwright-0.4.4/tests/test_issue_1147_legibility_pair_aggregation.py +417 -0
  72. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_issue_915_dense_case.py +7 -1
  73. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_issue_955_height_contingency.py +7 -1
  74. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_label_provenance.py +0 -1
  75. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_layout.py +242 -0
  76. draftwright-0.4.4/tests/test_leader_footprint.py +170 -0
  77. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_make_drawing.py +210 -27
  78. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_parameter_id.py +39 -0
  79. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_pmi.py +1 -0
  80. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_private_test_attr_reads.py +1 -0
  81. draftwright-0.4.4/tests/test_quality_components.py +170 -0
  82. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_recogniser_contract.py +12 -1
  83. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_recognition_manifest.py +16 -1
  84. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_recognition_result.py +8 -0
  85. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_registry.py +43 -2
  86. draftwright-0.4.4/tests/test_repack_geometry_seam.py +63 -0
  87. draftwright-0.4.4/tests/test_shared_box_memo.py +101 -0
  88. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_sheet_emit.py +42 -0
  89. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_sheet_identity_invariant.py +2 -0
  90. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_sheet_tables.py +114 -2
  91. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_slot_completeness.py +93 -3
  92. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_strip_layout.py +13 -1
  93. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_suppression_ledger.py +12 -20
  94. draftwright-0.4.4/tests/test_workflows.py +108 -0
  95. draftwright-0.4.2/src/draftwright/annotations/balloons.py +0 -342
  96. draftwright-0.4.2/src/draftwright/linting/issues.py +0 -29
  97. draftwright-0.4.2/tests/test_workflows.py +0 -70
  98. {draftwright-0.4.2 → draftwright-0.4.4}/.gitignore +0 -0
  99. {draftwright-0.4.2 → draftwright-0.4.4}/LICENSE +0 -0
  100. {draftwright-0.4.2 → draftwright-0.4.4}/skills/SKILL.md +0 -0
  101. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/_pmi_part21.py +0 -0
  102. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/annotate.py +0 -0
  103. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/annotations/__init__.py +0 -0
  104. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/annotations/sections.py +0 -0
  105. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/export.py +0 -0
  106. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/fits.py +0 -0
  107. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
  108. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
  109. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
  110. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
  111. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/fonts/__init__.py +0 -0
  112. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/intents.py +0 -0
  113. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/linting/channel_coverage.py +0 -0
  114. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/linting/flat_coverage.py +0 -0
  115. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/linting/pmi_coverage.py +0 -0
  116. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/linting/profiled_bore_coverage.py +0 -0
  117. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/linting/suggest.py +0 -0
  118. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/make_drawing.py +0 -0
  119. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/pmi.py +0 -0
  120. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/projection.py +0 -0
  121. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/py.typed +0 -0
  122. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/recognition/_record.py +0 -0
  123. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/recognition/chamfers.py +0 -0
  124. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/recognition/fillets.py +0 -0
  125. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/recognition/flats.py +0 -0
  126. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/recognition/grooves.py +0 -0
  127. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/recognition/levels.py +0 -0
  128. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/recognition/pads.py +0 -0
  129. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/recognition/plates.py +0 -0
  130. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/recognition/profiled_bores.py +0 -0
  131. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/recognition/turned.py +0 -0
  132. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/repair.py +0 -0
  133. {draftwright-0.4.2 → draftwright-0.4.4}/src/draftwright/score.py +0 -0
  134. {draftwright-0.4.2 → draftwright-0.4.4}/tests/_kernel.py +0 -0
  135. {draftwright-0.4.2 → draftwright-0.4.4}/tests/_layout_sig.py +0 -0
  136. {draftwright-0.4.2 → draftwright-0.4.4}/tests/conftest.py +0 -0
  137. {draftwright-0.4.2 → draftwright-0.4.4}/tests/fixtures/grm03_thumbwheel_drive_screw.step +0 -0
  138. {draftwright-0.4.2 → draftwright-0.4.4}/tests/fixtures/if_step_flat_across_cylinder.step +0 -0
  139. {draftwright-0.4.2 → draftwright-0.4.4}/tests/fixtures/issue_1058_wheel_rh.step +0 -0
  140. {draftwright-0.4.2 → draftwright-0.4.4}/tests/fixtures/issue_909_basic_part_design_017_body.step +0 -0
  141. {draftwright-0.4.2 → draftwright-0.4.4}/tests/fixtures/issue_915_case_study_2.step +0 -0
  142. {draftwright-0.4.2 → draftwright-0.4.4}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
  143. {draftwright-0.4.2 → draftwright-0.4.4}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
  144. {draftwright-0.4.2 → draftwright-0.4.4}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
  145. {draftwright-0.4.2 → draftwright-0.4.4}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
  146. {draftwright-0.4.2 → draftwright-0.4.4}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
  147. {draftwright-0.4.2 → draftwright-0.4.4}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
  148. {draftwright-0.4.2 → draftwright-0.4.4}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
  149. {draftwright-0.4.2 → draftwright-0.4.4}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
  150. {draftwright-0.4.2 → draftwright-0.4.4}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
  151. {draftwright-0.4.2 → draftwright-0.4.4}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
  152. {draftwright-0.4.2 → draftwright-0.4.4}/tests/fixtures/tuner_jig_blind_obround_pockets.step +0 -0
  153. {draftwright-0.4.2 → draftwright-0.4.4}/tests/refactor_golden/centered_rebate.json +0 -0
  154. {draftwright-0.4.2 → draftwright-0.4.4}/tests/refactor_golden/chamfered.json +0 -0
  155. {draftwright-0.4.2 → draftwright-0.4.4}/tests/refactor_golden/filleted.json +0 -0
  156. {draftwright-0.4.2 → draftwright-0.4.4}/tests/refactor_golden/grooved_shaft.json +0 -0
  157. {draftwright-0.4.2 → draftwright-0.4.4}/tests/refactor_golden/hex_bar.json +0 -0
  158. {draftwright-0.4.2 → draftwright-0.4.4}/tests/refactor_golden/prismatic_ladder.json +0 -0
  159. {draftwright-0.4.2 → draftwright-0.4.4}/tests/refactor_golden/turned_stepped.json +0 -0
  160. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_agents_guide.py +0 -0
  161. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_api_docs.py +0 -0
  162. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_architecture_docs.py +0 -0
  163. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_balloon_ring_standoff.py +0 -0
  164. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_carve_free_position_callers.py +0 -0
  165. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_counting_calls.py +0 -0
  166. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_declared_recognition_gate.py +0 -0
  167. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_dense_sheet_canary.py +0 -0
  168. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_deprecation_dates.py +0 -0
  169. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_detect_once.py +0 -0
  170. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_detect_registry.py +0 -0
  171. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_dimension_role_vocabulary.py +0 -0
  172. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_drawing_encapsulation.py +0 -0
  173. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_e2e_slice.py +0 -0
  174. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_export_dxf_curves.py +0 -0
  175. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_export_dxf_zoom.py +0 -0
  176. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_fillets_adjacency.py +0 -0
  177. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_fits.py +0 -0
  178. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_flat_stock_identity.py +0 -0
  179. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_flat_stock_opposition.py +0 -0
  180. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_grid_lattice_convention.py +0 -0
  181. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_import_boundaries.py +0 -0
  182. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_issue_1000_envelope_reuse.py +0 -0
  183. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_issue_676_polygonal_boss.py +0 -0
  184. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_issue_676_polygonal_boss_declare.py +0 -0
  185. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_issue_885_prismatic_coverage.py +0 -0
  186. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_issue_909_sloped_profile.py +0 -0
  187. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_issue_916_pocket_leader.py +0 -0
  188. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_issue_917_open_channel.py +0 -0
  189. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_issue_958_cross_solid_recognition.py +0 -0
  190. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_layout_cleanliness.py +0 -0
  191. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_layout_hypothesis.py +0 -0
  192. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_layout_property.py +0 -0
  193. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_lint_box_cache.py +0 -0
  194. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_lint_reconciliation.py +0 -0
  195. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_lint_structural.py +0 -0
  196. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_linting.py +0 -0
  197. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_location_vocabulary.py +0 -0
  198. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_note_verb.py +0 -0
  199. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_object_aspects.py +0 -0
  200. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_occupancy_boxes.py +0 -0
  201. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_part_model.py +0 -0
  202. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_pitch_dim_footprint.py +0 -0
  203. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_pmi_datum_lowering.py +0 -0
  204. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_pmi_gtol_lowering.py +0 -0
  205. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_pmi_part21.py +0 -0
  206. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_pocket_pattern.py +0 -0
  207. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_pocket_pattern_recognition.py +0 -0
  208. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_principal_profile_classifier.py +0 -0
  209. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_private_test_imports.py +0 -0
  210. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_recognition.py +0 -0
  211. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_refactor_golden.py +0 -0
  212. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_render_seam.py +0 -0
  213. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_score.py +0 -0
  214. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_script_detail_parity.py +0 -0
  215. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_sheet_gdt.py +0 -0
  216. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_sheet_notes.py +0 -0
  217. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_sheet_of.py +0 -0
  218. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_sheet_section.py +0 -0
  219. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_slanted_blind_step.py +0 -0
  220. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_slot_pattern.py +0 -0
  221. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_slot_pattern_recognition.py +0 -0
  222. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_soft_deprecation.py +0 -0
  223. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_solve_trace.py +0 -0
  224. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_suppression_marks.py +0 -0
  225. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_tolerances.py +0 -0
  226. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_turned_steps.py +0 -0
  227. {draftwright-0.4.2 → draftwright-0.4.4}/tests/test_witness_label_reconciliation.py +0 -0
@@ -2,6 +2,142 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## v0.4.4 — 2026-08-15
6
+
7
+ **A semantic-completeness and fail-closed layout patch release.** Hole requirements and drawing
8
+ quality are now machine-readable, hole-table replacement is transactional, explicit scales
9
+ preserve required output, notes tables use all viable sheet space, and gear declarations,
10
+ recognition evidence, and callout metadata improve diagnostic trust.
11
+
12
+ ### Added
13
+
14
+ - **External spur gears can be declared through the `Sheet` façade** (#1086). The declaration
15
+ records the correlated ISO 1328-1 manufacturing requirements, renders a structured gear-data
16
+ table, and survives script emission, declaration, export, and lint without inferring gear data
17
+ that the caller did not supply.
18
+
19
+ - **Recognition carries serialisable repeating radial-profile evidence** (#1087). Exact
20
+ rotational whole-wire proof can reconcile a declared gear with its physical profile while
21
+ preserving ambiguity across separate bodies and without inventing module, pressure angle, or
22
+ other manufacturing intent.
23
+
24
+ - **Explicit drawing scales now protect required annotation completeness** (#1146).
25
+ `build_drawing(...)`, `make_drawing(...)`, `Sheet(...)`, and the CLI accept
26
+ `scale_policy="fallback"|"strict"|"permissive"`. The safe default retries preferred ISO
27
+ 5455 reductions and returns the largest complete scale; strict mode raises with structured
28
+ requirement blockers, while permissive mode is an explicit warned opt-in to the historical
29
+ best-effort result. Every returned `Drawing.scale_decision` reports the requested and
30
+ effective scales, status, attempted scales, and any semantic placement blockers.
31
+
32
+ - **Hole and hole-pattern requirements now participate in semantic completeness accounting**
33
+ (#1143). `lint_summary()["quality"]["completeness"]` reconciles recognition-owned bore,
34
+ depth/through, grouping, pattern, and location requirements to placed, suppressed, dropped,
35
+ missing, or explicitly unverifiable outcomes. Ambiguous declared/automatic correspondence
36
+ fails closed without shrinking the physical denominator. Z-normal hole locations retain ADR
37
+ 0016's feature-level `location.location` addressability while structured physical evidence
38
+ distinguishes the independently required X and Y ordinates for critique.
39
+
40
+ - **`lint_summary()` exposes drawing-quality evidence as separate completeness, restraint,
41
+ and legibility components** (#1127). The legacy `score` remains byte-compatible and is also
42
+ returned as the honestly named `diagnostic_score`; no composite quality verdict is invented.
43
+ Completeness scores recognition-owned requirements in the families with semantic outcome
44
+ ledgers and states that conditional scope explicitly; recognized families not yet scored are
45
+ listed separately. Its scalar is `audited_score`, not `score`, because a feature recognition
46
+ missed never became a requirement — so a part with a recogniser gap can reach 1.0, and the
47
+ qualifier belongs where it survives being quoted. The block also lists what its denominator
48
+ `excludes` and counts `unrecognised_geometry_reports` (a floor on that gap, not a measure).
49
+ It is not a completion gate. Legibility includes only layout and placement diagnostics, and
50
+ scores information-severity ones — "place what fits" drops, a leader crossing a silhouette —
51
+ against the warning floor, so it cannot read 1.0 while itemising output it calls unreadable.
52
+ A drop is identified by its `*_dropped` code suffix unless its producer records an explicit
53
+ `outcome_stage`, so a drop code added later counts without being registered anywhere.
54
+ Restraint fails closed as unavailable until measurement provenance can classify every
55
+ annotation.
56
+
57
+ ### Fixed
58
+
59
+ - **Generic notes/data tables use every viable free sheet region and explain failed placement**
60
+ (#1145). A drawn sheet frame or zone ruler now defines the usable inset instead of becoming a
61
+ page-sized solid obstacle, so a measured four-row notes block can use clear lower-left A4
62
+ space. Tables retain the drafting preset's external text clearance from named and anonymous
63
+ existing annotations. Failed placement diagnostics report the measured page-space footprint,
64
+ attempted candidate regions, and the named obstacles or clearance bands that blocked the
65
+ nearest candidates without materialising the candidate Cartesian product.
66
+
67
+ - **Automatic hole-table escalation is transactional per semantic requirement** (#1144). The
68
+ engine suppresses replaceable callouts and location dimensions only after the table fits and
69
+ every visible row has its required feature-owned balloon. A failed or partial balloon attempt
70
+ rolls the shared table back as a unit and restores the exact annotations, ordering, registry
71
+ identities, coverage, and issues. Precise ring, glyph, and leader-segment tests keep placement
72
+ clear of retained callout labels, leader shafts, and existing public-balloon components without
73
+ changing the existing additive `add_hole_table()`/`add_balloons()` behavior; malformed component
74
+ metadata—including Boolean cardinalities or coordinate payloads—falls back conservatively or fails the
75
+ optional replacement closed. New balloon shafts must also remain mutually crossing-free.
76
+ Compound, thread,
77
+ profile, pattern, tolerance, and fit facts the table cannot state remain active, while
78
+ independently replaceable X/Y locations may still move to the table. Guarded interval solving
79
+ has deterministic pre-lane, pre-carve, and work/state budgets; an adversarially fragmented label
80
+ inventory or band fails the replacement closed before excessive work or allocation. Required row-key
81
+ balloons take priority over optional non-certifying pattern markers, so an independently complete
82
+ table cannot be displaced by an auxiliary marker.
83
+ Hole outcomes identify the winning representation per requirement;
84
+ mixed table/feature evidence deliberately claims no single winner. Exceptions restore those
85
+ semantic markers, and automatic escalation fails closed rather than overwrite an existing
86
+ annotation that owns a reserved table or balloon name.
87
+
88
+ - **Generated hole-callout leaders expose their exact rendered semantic text through the public
89
+ `Leader.label` field** (#1142). Geometry and placement are unchanged, and structured coverage
90
+ remains authoritative instead of requiring downstream tools to parse display text.
91
+
92
+ - **Leader placement probes use analytic footprint arithmetic instead of constructing temporary
93
+ OCCT geometry** (#1138). Representative probe-heavy layouts are substantially faster while
94
+ preserving custom-style containment and placement behavior.
95
+
96
+ ### Changed
97
+
98
+ - **Pairwise lint detail no longer multiplies the legibility score penalty** (#1147). Raw
99
+ severity and code counts retain their existing finding-level semantics, and every offending
100
+ pair remains in `issues`. The legibility component now also reports its primary issue counts
101
+ and scores one producer-identified annotation/failure mechanism once, with `affected_pairs`
102
+ exposing how many raw pair findings were aggregated. The legacy top-level `score` and
103
+ `diagnostic_score` remain unchanged.
104
+
105
+ - **Pull-request validation now reports a stable aggregate `ci-ok` check and runs the supported
106
+ Python matrix on Linux** (#1134/#1136). Weekly, manual, and labelled workflows retain the
107
+ cross-platform escape hatch, reducing routine Actions cost without narrowing supported Python
108
+ coverage.
109
+
110
+ ## v0.4.3 — 2026-08-09
111
+
112
+ **A trust and validation-throughput patch release.** Whole-part hexagonal stock now receives its
113
+ manufacturing definition, assembly recognition no longer combines evidence across bodies, and
114
+ performance validation is both more stable and faster to run.
115
+
116
+ ### Added
117
+
118
+ - **Whole-part regular hexagonal stock is recognised independently of an attached boss and
119
+ dimensioned as `HEX … A/F` plus its axial length** (#1082). In-plane rotation preserves the
120
+ geometric across-flats size instead of substituting an orientation-dependent bounding-box
121
+ width. Automatic and declared paths share the same compiler and placement machinery, while
122
+ completeness lint distinguishes placed, authored-suppressed, dropped, missing, and ambiguous
123
+ outcomes.
124
+
125
+ ### Fixed
126
+
127
+ - **Slot and pocket patterns cannot group members from separate assembly bodies** (#1073).
128
+ Recognition carries solid ownership through grouping and fails closed when ownership is
129
+ unavailable, preventing plausible-looking phantom patterns across disconnected parts.
130
+
131
+ - **The annotation-obstacle performance guard uses deterministic work-count evidence instead of
132
+ a wall-clock threshold** (#1071). The regression test remains load-bearing under mutation but
133
+ no longer flakes because of machine load or suite ordering.
134
+
135
+ ### Changed
136
+
137
+ - **The post-merge slow tier distributes individual tests across workers** (#1108), using
138
+ `pytest-xdist`'s load scheduling after local benchmark evidence showed a material speed-up
139
+ without weakening fixture coverage. Hosted slow validation remains the single post-merge gate.
140
+
5
141
  ## v0.4.2 — 2026-08-09
6
142
 
7
143
  **The trustworthy manufacturing drawings release.** AP242 PMI is now accounted for from its
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: draftwright
3
- Version: 0.4.2
3
+ Version: 0.4.4
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
@@ -743,6 +743,7 @@ Choose formats, scale, and page; or emit an editable drawing script:
743
743
  ```
744
744
  draftwright my_part.step --format pdf,dxf # also: svg, all
745
745
  draftwright my_part.step --scale 2 --page A3 # override the auto scale / page
746
+ draftwright my_part.step --scale 1 --page A4 --scale-policy strict
746
747
  draftwright my_part.step --script # write an editable declarative Sheet script
747
748
  ```
748
749
 
@@ -895,13 +896,18 @@ installed automatically as a dependency.
895
896
  ### Scale and page control
896
897
 
897
898
  ```python
898
- from draftwright import choose_scale
899
+ from draftwright import build_drawing, choose_scale
899
900
 
900
901
  # Auto-select the best ISO/ASME standard scale for an A3 sheet
901
902
  scale, page_w, page_h, n_steps = choose_scale(80, 60, 20, page="A3")
902
903
 
903
- # Override
904
- make_drawing(part, out="drawing", scale=2.0, page="A2")
904
+ # Override. Required annotations are protected by the default fallback policy.
905
+ dwg = build_drawing(part, scale=1.0, page="A4")
906
+ print(dwg.scale_decision) # requested/effective scale, attempts, blockers
907
+
908
+ # Refuse an incompatible request, or explicitly retain historical best-effort output.
909
+ build_drawing(part, scale=1.0, page="A4", scale_policy="strict")
910
+ build_drawing(part, scale=1.0, page="A4", scale_policy="permissive")
905
911
  ```
906
912
 
907
913
  ### Edit, critique, and self-repair
@@ -916,10 +922,40 @@ dwg = build_drawing(part)
916
922
  env = next(f for f in dwg.model().features if f.kind == "envelope")
917
923
  dwg.dimension(env, "length", role="width", side="below", pin=True)
918
924
 
919
- crit = dwg.lint_summary() # {"passed", "score", "by_code", "issues":[…suggestion]}
925
+ crit = dwg.lint_summary()
926
+ # Legacy diagnostic severity heuristic (not overall drawing quality):
927
+ assert crit["diagnostic_score"] == crit["score"]
928
+ # Independently observable components; no composite quality score is invented:
929
+ completeness = crit["quality"]["completeness"]
930
+ # Named for its denominator: the requirements Draftwright recognized AND can audit.
931
+ # 1.0 does NOT mean the drawing is complete — it means every requirement recognition did
932
+ # identify was placed, however much of the part it missed. What was never recognized never
933
+ # became a requirement. (Recognized nothing auditable at all? available is False, not 1.0.)
934
+ audited = completeness["audited_score"]
935
+ assert completeness["scope"] == "audited_recognized_requirements"
936
+ completeness["excludes"] # what the denominator cannot see
937
+ completeness["unrecognised_geometry_reports"] # a floor on that gap, never a measure
938
+ legibility = crit["quality"]["legibility"]
939
+ # Raw pair findings remain available and keep the original count semantics.
940
+ legibility["warnings"], legibility["by_code"], crit["issues"]
941
+ # The legibility scalar uses primary issues: one affected annotation/failure mechanism even
942
+ # when several pairwise comparisons found it. These fields make that denominator explicit.
943
+ legibility["primary_issues"], legibility["primary_by_code"], legibility["affected_pairs"]
944
+ assert legibility["score_inventory"] == "primary_issues"
945
+ restraint = crit["quality"]["restraint"] # unavailable until provenance is complete
920
946
  dwg.repair() # auto-fix mechanically-fixable lint; never worsens
921
947
  ```
922
948
 
949
+ Completeness is strict about authored omissions: removing a dimension from a complete authored
950
+ set records a `suppressed` requirement and lowers the audited score. The evidence distinguishes
951
+ that choice from a placement failure, but cannot certify that the omission is
952
+ engineering-correct; plain deletion is not an accepted-waiver mechanism.
953
+
954
+ **`audited_score` is not a completion gate.** It answers "of the requirements we recognised and
955
+ can audit, how many were placed?" — so a part can score 1.0 with whole features missing from the
956
+ drawing, if recognition never identified them. Gate on issue codes and severities, and read
957
+ `excludes` and `unrecognised_geometry_reports` for what the denominator cannot account for.
958
+
923
959
  Each `LintIssue` carries a domain-meaningful `code` and, when computable, a
924
960
  ready-to-apply `suggestion`. See `docs/adr/` for the design (deterministic
925
961
  generation, the lint→repair loop, and collect-then-solve placement).
@@ -44,6 +44,7 @@ Choose formats, scale, and page; or emit an editable drawing script:
44
44
  ```
45
45
  draftwright my_part.step --format pdf,dxf # also: svg, all
46
46
  draftwright my_part.step --scale 2 --page A3 # override the auto scale / page
47
+ draftwright my_part.step --scale 1 --page A4 --scale-policy strict
47
48
  draftwright my_part.step --script # write an editable declarative Sheet script
48
49
  ```
49
50
 
@@ -196,13 +197,18 @@ installed automatically as a dependency.
196
197
  ### Scale and page control
197
198
 
198
199
  ```python
199
- from draftwright import choose_scale
200
+ from draftwright import build_drawing, choose_scale
200
201
 
201
202
  # Auto-select the best ISO/ASME standard scale for an A3 sheet
202
203
  scale, page_w, page_h, n_steps = choose_scale(80, 60, 20, page="A3")
203
204
 
204
- # Override
205
- make_drawing(part, out="drawing", scale=2.0, page="A2")
205
+ # Override. Required annotations are protected by the default fallback policy.
206
+ dwg = build_drawing(part, scale=1.0, page="A4")
207
+ print(dwg.scale_decision) # requested/effective scale, attempts, blockers
208
+
209
+ # Refuse an incompatible request, or explicitly retain historical best-effort output.
210
+ build_drawing(part, scale=1.0, page="A4", scale_policy="strict")
211
+ build_drawing(part, scale=1.0, page="A4", scale_policy="permissive")
206
212
  ```
207
213
 
208
214
  ### Edit, critique, and self-repair
@@ -217,10 +223,40 @@ dwg = build_drawing(part)
217
223
  env = next(f for f in dwg.model().features if f.kind == "envelope")
218
224
  dwg.dimension(env, "length", role="width", side="below", pin=True)
219
225
 
220
- crit = dwg.lint_summary() # {"passed", "score", "by_code", "issues":[…suggestion]}
226
+ crit = dwg.lint_summary()
227
+ # Legacy diagnostic severity heuristic (not overall drawing quality):
228
+ assert crit["diagnostic_score"] == crit["score"]
229
+ # Independently observable components; no composite quality score is invented:
230
+ completeness = crit["quality"]["completeness"]
231
+ # Named for its denominator: the requirements Draftwright recognized AND can audit.
232
+ # 1.0 does NOT mean the drawing is complete — it means every requirement recognition did
233
+ # identify was placed, however much of the part it missed. What was never recognized never
234
+ # became a requirement. (Recognized nothing auditable at all? available is False, not 1.0.)
235
+ audited = completeness["audited_score"]
236
+ assert completeness["scope"] == "audited_recognized_requirements"
237
+ completeness["excludes"] # what the denominator cannot see
238
+ completeness["unrecognised_geometry_reports"] # a floor on that gap, never a measure
239
+ legibility = crit["quality"]["legibility"]
240
+ # Raw pair findings remain available and keep the original count semantics.
241
+ legibility["warnings"], legibility["by_code"], crit["issues"]
242
+ # The legibility scalar uses primary issues: one affected annotation/failure mechanism even
243
+ # when several pairwise comparisons found it. These fields make that denominator explicit.
244
+ legibility["primary_issues"], legibility["primary_by_code"], legibility["affected_pairs"]
245
+ assert legibility["score_inventory"] == "primary_issues"
246
+ restraint = crit["quality"]["restraint"] # unavailable until provenance is complete
221
247
  dwg.repair() # auto-fix mechanically-fixable lint; never worsens
222
248
  ```
223
249
 
250
+ Completeness is strict about authored omissions: removing a dimension from a complete authored
251
+ set records a `suppressed` requirement and lowers the audited score. The evidence distinguishes
252
+ that choice from a placement failure, but cannot certify that the omission is
253
+ engineering-correct; plain deletion is not an accepted-waiver mechanism.
254
+
255
+ **`audited_score` is not a completion gate.** It answers "of the requirements we recognised and
256
+ can audit, how many were placed?" — so a part can score 1.0 with whole features missing from the
257
+ drawing, if recognition never identified them. Gate on issue codes and severities, and read
258
+ `excludes` and `unrecognised_geometry_reports` for what the denominator cannot account for.
259
+
224
260
  Each `LintIssue` carries a domain-meaningful `code` and, when computable, a
225
261
  ready-to-apply `suggestion`. See `docs/adr/` for the design (deterministic
226
262
  generation, the lint→repair loop, and collect-then-solve placement).
@@ -34,6 +34,7 @@ architecture** table; open retired or superseded records only for design history
34
34
  | [0015](0015-part-drawing-compiler-as-built.md) | The part-drawing compiler, as built | Use one detected-or-declared feature IR and planner-fed dimension groups as the compiler waist. | Accepted; supersedes 0008 | `test_part_model.py`, `test_detect_once.py`, `test_import_boundaries.py` |
35
35
  | [0016](0016-declared-dimensioning-intent.md) | Declared dimensioning intent: capture what to measure, let the engine place it | Declare which measurements matter as scale-independent intent routed through the planner and corridor solve; never hardcode dimension geometry. | Accepted; epic #867 complete; phase 6 landed (#940) | `test_compiled_plan_boundary.py`, `test_label_provenance.py`, `test_sheet_emit.py`, `test_add_dimension.py` |
36
36
  | [0017](0017-recognition-inventory-correspondence-and-measurement-provenance.md) | One recognition result per run; correspondence is evidence-gated | Produce one explicit recognition result owned by `BuildState`; require vertical-slice evidence before generalising correspondence, identity, requirements, outcomes, or reconciliation. | Accepted; ownership phase landed, extensions gated by #1018 | `test_recognition_manifest.py`, `test_recognition_result.py`, `test_declared_recognition_gate.py` |
37
+ | [0018](0018-requirement-driven-view-planning-and-editable-sheet-layout.md) | Requirement-driven view planning and editable sheet layout | Use one `ViewSpec` vocabulary and planner with distinct authored `ViewConstraints` and immutable `ResolvedViewPlan`; jointly validate views, typography, convention, scale, paper and layout against requirement survival. If accepted, supersedes ADR 0004's fixed-topology assumption while retaining compose-then-pack. | Proposed; tracked by #1130 | Required guards are listed in the ADR |
37
38
 
38
39
  ## Historical records
39
40
 
@@ -47,8 +48,8 @@ architecture** table; open retired or superseded records only for design history
47
48
 
48
49
  - Compiler and state ownership: 0001 → 0005 → 0015.
49
50
  - Recognition and public declaration: 0007 → 0013 → 0011 → 0015.
50
- - Layout and placement: 0004 → 0014 → 0012.
51
- - Declared intent and the editable surface: 0001 → 0011 → 0012 → 0016.
51
+ - Layout and placement: 0004 → 0014 → 0012 → 0018.
52
+ - Declared intent and the editable surface: 0001 → 0011 → 0012 → 0016 → 0018.
52
53
  - Quality and correction: 0002, with provenance from 0010.
53
54
  - Recognition correspondence and completeness: 0007 → 0013 → 0015 → 0017.
54
55
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "draftwright"
7
- version = "0.4.2"
7
+ version = "0.4.4"
8
8
  description = "Automated technical-drawing generation for build123d"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -24,6 +24,7 @@ from typing import TYPE_CHECKING
24
24
  # on every TAB press (#313). Each name maps to the submodule that provides it.
25
25
  _LAZY = {
26
26
  "build_drawing": "draftwright.builder",
27
+ "ScaleIncompatibilityError": "draftwright.builder",
27
28
  "make_drawing": "draftwright.builder",
28
29
  "Drawing": "draftwright.drawing",
29
30
  "FeatureInfo": "draftwright.drawing",
@@ -40,6 +41,7 @@ _LAZY = {
40
41
  # dependency-free `_warnings` leaf for that second reason: defined in `_core` it cost ~6 s
41
42
  # to reach, and the pytest filterwarnings entry naming it paid that on every invocation.
42
43
  "SoftDeprecationWarning": "draftwright._warnings",
44
+ "ScaleCompletenessWarning": "draftwright._warnings",
43
45
  }
44
46
 
45
47
 
@@ -76,8 +78,8 @@ _sys.modules[__name__].__class__ = _DraftwrightModule
76
78
 
77
79
 
78
80
  if TYPE_CHECKING: # static analysers / IDEs — no runtime import, no kernel cost
79
- from draftwright._warnings import SoftDeprecationWarning
80
- from draftwright.builder import build_drawing, make_drawing
81
+ from draftwright._warnings import ScaleCompletenessWarning, SoftDeprecationWarning
82
+ from draftwright.builder import ScaleIncompatibilityError, build_drawing, make_drawing
81
83
  from draftwright.compose import choose_scale
82
84
  from draftwright.drawing import Drawing, FeatureInfo
83
85
  from draftwright.linting import lint_feature_coverage
@@ -106,6 +108,8 @@ __all__ = [
106
108
  "PmiRecord",
107
109
  "PmiSourceEntity",
108
110
  "Sheet",
111
+ "ScaleIncompatibilityError",
112
+ "ScaleCompletenessWarning",
109
113
  "build_drawing",
110
114
  "choose_scale",
111
115
  "extract_pmi",
@@ -280,6 +280,22 @@ def _tag_sequence(n):
280
280
 
281
281
  _DIAM_RE = re.compile(r"[øØ⌀]\s*(\d+(?:\.\d+)?)")
282
282
 
283
+
284
+ def _annotation_diameter_sources(annotation) -> tuple[tuple[float, ...], tuple[float, ...]]:
285
+ """Return ``(structured, text)`` diameter mentions for one annotation.
286
+
287
+ Engine-produced annotations with ``covers_diameters`` describe their physical-feature
288
+ correspondence structurally. Their formatted labels may contain other diameters (for
289
+ example ``ø60 BC``), so text parsing is a legacy/unstructured fallback and is never
290
+ combined with structured coverage (ADR 0017 / #1142).
291
+ """
292
+ structured = tuple(float(value) for value in getattr(annotation, "covers_diameters", ()))
293
+ if structured:
294
+ return structured, ()
295
+ label = getattr(annotation, "label", None) or ""
296
+ return (), tuple(float(match.group(1)) for match in _DIAM_RE.finditer(label))
297
+
298
+
283
299
  # A single-quoted label lifted from a lint message, e.g. "labels 'A' and 'B' …".
284
300
  # Shared by the #29 lint suggestions (linting.py) and the #30 repair loop.
285
301
  _QUOTED_RE = re.compile(r"'([^']*)'")
@@ -981,6 +997,7 @@ class Analysis:
981
997
  layout_n_steps: int
982
998
  layout_section: bool
983
999
  layout_table_sizes: tuple[tuple[float, float], ...]
1000
+ layout_required_tables: tuple[tuple[tuple[float, float], str], ...]
984
1001
  sv_right: float
985
1002
  iso_right_limit: float
986
1003
  SCALE: float
@@ -109,6 +109,18 @@ def _axis_letter_of(axis) -> str:
109
109
  return max(zip("xyz", axis, strict=True), key=lambda t: abs(t[1]))[0]
110
110
 
111
111
 
112
+ def _is_principal_axis(axis) -> bool:
113
+ """Whether *axis* is exactly along X, Y or Z after analytic-noise snapping.
114
+
115
+ ``Frame.axis`` can name only these directions. Exactness is evaluated after the same
116
+ ``1e-6`` component snap used for analytic STEP noise. A cosine-tolerance test would
117
+ admit a small but real second component (and millimetres of flattening over a long
118
+ part); requiring exactly one non-zero component expresses the representable contract.
119
+ """
120
+ snapped = [0.0 if abs(float(component)) < 1e-6 else float(component) for component in axis]
121
+ return sum(component != 0.0 for component in snapped) == 1
122
+
123
+
112
124
  def _axis_direction_components(axis: str, direction=None):
113
125
  """Validate a direction and return ``(raw, norm, dominant_index)``."""
114
126
  if axis not in "xyz" or len(axis) != 1:
@@ -269,11 +281,14 @@ def _segment_crosses_box(p1, p2, box) -> bool:
269
281
  return any(_seg_seg(p1, p2, corners[i], corners[(i + 1) % 4]) for i in range(4))
270
282
 
271
283
 
272
- def _segment_clips_box(p, q, box, pad=0.0) -> bool:
273
- """Liang–Barsky: does segment *p*→*q* intersect the *pad*-inflated AABB
274
- *box*? Inclusive at the boundary (a touch is a hit) — the tolerant form the
275
- lint checks use; :func:`_segment_crosses_box` is the strict placement-side
276
- sibling (#700)."""
284
+ def _segment_clip_extent(p, q, box, pad=0.0):
285
+ """AABB of the part of *p*→*q* inside the *pad*-inflated *box*.
286
+
287
+ Return ``None`` when the segment is disjoint. The Liang–Barsky clip is
288
+ inclusive at the boundary: lint uses the returned extent both as its
289
+ tolerant hit gate and to measure only the rendered part that actually
290
+ reaches a label (#1144).
291
+ """
277
292
  minx, miny, maxx, maxy = box[0] - pad, box[1] - pad, box[2] + pad, box[3] + pad
278
293
  x0, y0 = p
279
294
  dx, dy = q[0] - x0, q[1] - y0
@@ -281,15 +296,90 @@ def _segment_clips_box(p, q, box, pad=0.0) -> bool:
281
296
  for pp, qq in ((-dx, x0 - minx), (dx, maxx - x0), (-dy, y0 - miny), (dy, maxy - y0)):
282
297
  if abs(pp) < 1e-12:
283
298
  if qq < 0:
284
- return False
299
+ return None
285
300
  else:
286
301
  r = qq / pp
287
302
  if pp < 0:
288
303
  if r > t1:
289
- return False
304
+ return None
290
305
  t0 = max(t0, r)
291
306
  else:
292
307
  if r < t0:
293
- return False
308
+ return None
294
309
  t1 = min(t1, r)
295
- return t0 <= t1
310
+ if t0 > t1:
311
+ return None
312
+ first = (x0 + t0 * dx, y0 + t0 * dy)
313
+ second = (x0 + t1 * dx, y0 + t1 * dy)
314
+ return (
315
+ min(first[0], second[0]),
316
+ min(first[1], second[1]),
317
+ max(first[0], second[0]),
318
+ max(first[1], second[1]),
319
+ )
320
+
321
+
322
+ def _segment_clips_box(p, q, box, pad=0.0) -> bool:
323
+ """Liang–Barsky: does segment *p*→*q* intersect the *pad*-inflated AABB
324
+ *box*? Inclusive at the boundary (a touch is a hit) — the tolerant form the
325
+ lint checks use; :func:`_segment_crosses_box` is the strict placement-side
326
+ sibling (#700)."""
327
+ return _segment_clip_extent(p, q, box, pad=pad) is not None
328
+
329
+
330
+ def _segments_cross_or_overlap(a1, a2, b1, b2) -> bool:
331
+ """Whether two page-plane segments cross or overlap beyond a shared endpoint.
332
+
333
+ Proper interior crossings, a T-junction into the other segment's interior,
334
+ and collinear overlap are conflicts. Merely sharing one endpoint is not: two
335
+ leaders may legitimately terminate at the same feature/ring boundary.
336
+ """
337
+ epsilon = 1e-9
338
+
339
+ def cross(origin, first, second):
340
+ return (first[0] - origin[0]) * (second[1] - origin[1]) - (first[1] - origin[1]) * (
341
+ second[0] - origin[0]
342
+ )
343
+
344
+ def same(first, second):
345
+ return abs(first[0] - second[0]) <= epsilon and abs(first[1] - second[1]) <= epsilon
346
+
347
+ def on_segment(point, first, second):
348
+ return (
349
+ abs(cross(first, second, point)) <= epsilon
350
+ and min(first[0], second[0]) - epsilon
351
+ <= point[0]
352
+ <= max(first[0], second[0]) + epsilon
353
+ and min(first[1], second[1]) - epsilon
354
+ <= point[1]
355
+ <= max(first[1], second[1]) + epsilon
356
+ )
357
+
358
+ oa1 = cross(b1, b2, a1)
359
+ oa2 = cross(b1, b2, a2)
360
+ ob1 = cross(a1, a2, b1)
361
+ ob2 = cross(a1, a2, b2)
362
+ if oa1 * oa2 < -(epsilon**2) and ob1 * ob2 < -(epsilon**2):
363
+ return True
364
+
365
+ if all(abs(value) <= epsilon for value in (oa1, oa2, ob1, ob2)):
366
+ axis = (
367
+ 0
368
+ if max(abs(a2[0] - a1[0]), abs(b2[0] - b1[0]))
369
+ >= max(abs(a2[1] - a1[1]), abs(b2[1] - b1[1]))
370
+ else 1
371
+ )
372
+ overlap = min(max(a1[axis], a2[axis]), max(b1[axis], b2[axis])) - max(
373
+ min(a1[axis], a2[axis]), min(b1[axis], b2[axis])
374
+ )
375
+ return bool(overlap > epsilon)
376
+
377
+ for point, first, second in (
378
+ (a1, b1, b2),
379
+ (a2, b1, b2),
380
+ (b1, a1, a2),
381
+ (b2, a1, a2),
382
+ ):
383
+ if on_segment(point, first, second) and not (same(point, first) or same(point, second)):
384
+ return True
385
+ return False
@@ -37,3 +37,11 @@ class SoftDeprecationWarning(UserWarning):
37
37
  in notebooks, several test runners, and library-internal call paths, whereas this shows
38
38
  unconditionally.
39
39
  """
40
+
41
+
42
+ class ScaleCompletenessWarning(UserWarning):
43
+ """An explicit scale was changed or honored with required placement loss (#1146).
44
+
45
+ Callers can filter this policy decision independently of unrelated user warnings while
46
+ still inspecting the complete machine-readable record on ``Drawing.scale_decision``.
47
+ """