draftwright 0.4.4__tar.gz → 0.4.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 (229) hide show
  1. {draftwright-0.4.4 → draftwright-0.4.6}/CHANGELOG.md +39 -1
  2. {draftwright-0.4.4 → draftwright-0.4.6}/PKG-INFO +9 -5
  3. {draftwright-0.4.4 → draftwright-0.4.6}/README.md +7 -4
  4. {draftwright-0.4.4 → draftwright-0.4.6}/docs/adr/README.md +3 -3
  5. {draftwright-0.4.4 → draftwright-0.4.6}/pyproject.toml +3 -1
  6. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/_core.py +2 -1
  7. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/_geometry.py +88 -0
  8. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/analysis.py +8 -8
  9. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/annotations/from_model.py +20 -14
  10. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/annotations/holes.py +60 -9
  11. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/drawing.py +15 -6
  12. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/linting/channel_coverage.py +2 -1
  13. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/linting/coverage.py +44 -21
  14. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/linting/flat_coverage.py +2 -1
  15. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/linting/hole_coverage.py +2 -1
  16. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/linting/polygonal_stock_coverage.py +2 -1
  17. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/linting/profiled_bore_coverage.py +2 -1
  18. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/linting/slot_coverage.py +2 -1
  19. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/model/compiled.py +8 -1
  20. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/model/declare.py +8 -9
  21. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/model/detect.py +35 -34
  22. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/model/ir.py +19 -1
  23. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/model/planner.py +14 -7
  24. draftwright-0.4.6/src/draftwright/recognition/__init__.py +15 -0
  25. draftwright-0.4.6/src/draftwright/recognition_cache.py +26 -0
  26. draftwright-0.4.6/src/draftwright/score.py +10 -0
  27. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_declare.py +29 -17
  28. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_declared_recognition_gate.py +9 -9
  29. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_detect_once.py +2 -2
  30. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_detect_registry.py +2 -2
  31. draftwright-0.4.6/tests/test_external_recognition_boundary.py +69 -0
  32. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_fillets_adjacency.py +1 -2
  33. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_flat_completeness.py +2 -2
  34. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_flat_stock_identity.py +21 -5
  35. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_flat_stock_opposition.py +1 -2
  36. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_grid_lattice_convention.py +3 -3
  37. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_import_boundaries.py +1 -0
  38. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_issue_1058_wheel_profile.py +23 -23
  39. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_issue_1073_cross_body_patterns.py +5 -5
  40. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_issue_1082_polygonal_stock.py +5 -5
  41. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_issue_1143_hole_completeness.py +7 -7
  42. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_issue_1146_scale_completeness.py +3 -3
  43. draftwright-0.4.6/tests/test_issue_367_leader_ink.py +137 -0
  44. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_issue_676_polygonal_boss.py +2 -2
  45. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_issue_885_prismatic_coverage.py +21 -1
  46. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_issue_909_sloped_profile.py +1 -1
  47. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_issue_917_open_channel.py +1 -1
  48. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_issue_958_cross_solid_recognition.py +4 -4
  49. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_make_drawing.py +34 -46
  50. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_part_model.py +1 -1
  51. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_pocket_pattern_recognition.py +9 -9
  52. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_private_test_imports.py +5 -0
  53. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_quality_components.py +2 -1
  54. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_recogniser_contract.py +3 -4
  55. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_recognition.py +28 -30
  56. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_recognition_manifest.py +14 -14
  57. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_recognition_result.py +9 -8
  58. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_slanted_blind_step.py +3 -3
  59. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_slot_completeness.py +1 -1
  60. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_slot_pattern_recognition.py +6 -6
  61. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_strip_layout.py +52 -0
  62. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_turned_steps.py +1 -2
  63. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_workflows.py +14 -0
  64. draftwright-0.4.4/src/draftwright/recognition/__init__.py +0 -205
  65. draftwright-0.4.4/src/draftwright/recognition/_features.py +0 -1164
  66. draftwright-0.4.4/src/draftwright/recognition/_record.py +0 -28
  67. draftwright-0.4.4/src/draftwright/recognition/chamfers.py +0 -196
  68. draftwright-0.4.4/src/draftwright/recognition/countersinks.py +0 -206
  69. draftwright-0.4.4/src/draftwright/recognition/fillets.py +0 -183
  70. draftwright-0.4.4/src/draftwright/recognition/flats.py +0 -275
  71. draftwright-0.4.4/src/draftwright/recognition/grooves.py +0 -145
  72. draftwright-0.4.4/src/draftwright/recognition/levels.py +0 -346
  73. draftwright-0.4.4/src/draftwright/recognition/pads.py +0 -148
  74. draftwright-0.4.4/src/draftwright/recognition/plates.py +0 -154
  75. draftwright-0.4.4/src/draftwright/recognition/polygonal_bosses.py +0 -342
  76. draftwright-0.4.4/src/draftwright/recognition/profiled_bores.py +0 -368
  77. draftwright-0.4.4/src/draftwright/recognition/repeating_profiles.py +0 -393
  78. draftwright-0.4.4/src/draftwright/recognition/result.py +0 -293
  79. draftwright-0.4.4/src/draftwright/recognition/slots.py +0 -1465
  80. draftwright-0.4.4/src/draftwright/recognition/turned.py +0 -202
  81. draftwright-0.4.4/src/draftwright/score.py +0 -76
  82. {draftwright-0.4.4 → draftwright-0.4.6}/.gitignore +0 -0
  83. {draftwright-0.4.4 → draftwright-0.4.6}/LICENSE +0 -0
  84. {draftwright-0.4.4 → draftwright-0.4.6}/skills/SKILL.md +0 -0
  85. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/__init__.py +0 -0
  86. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/_pmi_part21.py +0 -0
  87. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/_warnings.py +0 -0
  88. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/annotate.py +0 -0
  89. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/annotations/__init__.py +0 -0
  90. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/annotations/_common.py +0 -0
  91. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/annotations/balloons.py +0 -0
  92. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/annotations/gears.py +0 -0
  93. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/annotations/orchestrator.py +0 -0
  94. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/annotations/sections.py +0 -0
  95. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/audit.py +0 -0
  96. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/builder.py +0 -0
  97. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/cli.py +0 -0
  98. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/compose.py +0 -0
  99. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/export.py +0 -0
  100. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/fits.py +0 -0
  101. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
  102. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
  103. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
  104. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
  105. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/fonts/__init__.py +0 -0
  106. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/intents.py +0 -0
  107. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/layout.py +0 -0
  108. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/linting/__init__.py +0 -0
  109. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/linting/gear_coverage.py +0 -0
  110. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/linting/issues.py +0 -0
  111. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/linting/pmi_coverage.py +0 -0
  112. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/linting/quality.py +0 -0
  113. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/linting/structural.py +0 -0
  114. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/linting/suggest.py +0 -0
  115. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/make_drawing.py +0 -0
  116. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/model/__init__.py +0 -0
  117. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/model/callout.py +0 -0
  118. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/pmi.py +0 -0
  119. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/projection.py +0 -0
  120. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/py.typed +0 -0
  121. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/registry.py +0 -0
  122. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/repair.py +0 -0
  123. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/sheet.py +0 -0
  124. {draftwright-0.4.4 → draftwright-0.4.6}/src/draftwright/sheet_emit.py +0 -0
  125. {draftwright-0.4.4 → draftwright-0.4.6}/tests/_kernel.py +0 -0
  126. {draftwright-0.4.4 → draftwright-0.4.6}/tests/_layout_sig.py +0 -0
  127. {draftwright-0.4.4 → draftwright-0.4.6}/tests/conftest.py +0 -0
  128. {draftwright-0.4.4 → draftwright-0.4.6}/tests/fixtures/grm03_thumbwheel_drive_screw.step +0 -0
  129. {draftwright-0.4.4 → draftwright-0.4.6}/tests/fixtures/if_step_flat_across_cylinder.step +0 -0
  130. {draftwright-0.4.4 → draftwright-0.4.6}/tests/fixtures/issue_1058_wheel_rh.step +0 -0
  131. {draftwright-0.4.4 → draftwright-0.4.6}/tests/fixtures/issue_909_basic_part_design_017_body.step +0 -0
  132. {draftwright-0.4.4 → draftwright-0.4.6}/tests/fixtures/issue_915_case_study_2.step +0 -0
  133. {draftwright-0.4.4 → draftwright-0.4.6}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
  134. {draftwright-0.4.4 → draftwright-0.4.6}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
  135. {draftwright-0.4.4 → draftwright-0.4.6}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
  136. {draftwright-0.4.4 → draftwright-0.4.6}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
  137. {draftwright-0.4.4 → draftwright-0.4.6}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
  138. {draftwright-0.4.4 → draftwright-0.4.6}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
  139. {draftwright-0.4.4 → draftwright-0.4.6}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
  140. {draftwright-0.4.4 → draftwright-0.4.6}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
  141. {draftwright-0.4.4 → draftwright-0.4.6}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
  142. {draftwright-0.4.4 → draftwright-0.4.6}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
  143. {draftwright-0.4.4 → draftwright-0.4.6}/tests/fixtures/tuner_jig_blind_obround_pockets.step +0 -0
  144. {draftwright-0.4.4 → draftwright-0.4.6}/tests/refactor_golden/bracket_section.json +0 -0
  145. {draftwright-0.4.4 → draftwright-0.4.6}/tests/refactor_golden/centered_rebate.json +0 -0
  146. {draftwright-0.4.4 → draftwright-0.4.6}/tests/refactor_golden/chamfered.json +0 -0
  147. {draftwright-0.4.4 → draftwright-0.4.6}/tests/refactor_golden/filleted.json +0 -0
  148. {draftwright-0.4.4 → draftwright-0.4.6}/tests/refactor_golden/flange_dense.json +0 -0
  149. {draftwright-0.4.4 → draftwright-0.4.6}/tests/refactor_golden/grid_plate.json +0 -0
  150. {draftwright-0.4.4 → draftwright-0.4.6}/tests/refactor_golden/grooved_shaft.json +0 -0
  151. {draftwright-0.4.4 → draftwright-0.4.6}/tests/refactor_golden/hex_bar.json +0 -0
  152. {draftwright-0.4.4 → draftwright-0.4.6}/tests/refactor_golden/holed_slot.json +0 -0
  153. {draftwright-0.4.4 → draftwright-0.4.6}/tests/refactor_golden/pocketed.json +0 -0
  154. {draftwright-0.4.4 → draftwright-0.4.6}/tests/refactor_golden/prismatic_ladder.json +0 -0
  155. {draftwright-0.4.4 → draftwright-0.4.6}/tests/refactor_golden/scattered_plate.json +0 -0
  156. {draftwright-0.4.4 → draftwright-0.4.6}/tests/refactor_golden/side_drilled.json +0 -0
  157. {draftwright-0.4.4 → draftwright-0.4.6}/tests/refactor_golden/turned_stepped.json +0 -0
  158. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_add_dimension.py +0 -0
  159. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_agents_guide.py +0 -0
  160. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_api_docs.py +0 -0
  161. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_architecture_docs.py +0 -0
  162. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_audit_differential.py +0 -0
  163. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_balloon_ring_standoff.py +0 -0
  164. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_carve_free_position_callers.py +0 -0
  165. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_compiled_plan_boundary.py +0 -0
  166. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_counting_calls.py +0 -0
  167. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_dense_sheet_canary.py +0 -0
  168. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_deprecation_dates.py +0 -0
  169. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_dimension_role_vocabulary.py +0 -0
  170. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_drawing_encapsulation.py +0 -0
  171. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_e2e_slice.py +0 -0
  172. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_e2e_standards.py +0 -0
  173. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_export_dxf_curves.py +0 -0
  174. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_export_dxf_zoom.py +0 -0
  175. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_fits.py +0 -0
  176. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_gdt_placement.py +0 -0
  177. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_issue_1000_envelope_reuse.py +0 -0
  178. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_issue_1086_declared_gears.py +0 -0
  179. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_issue_1142_hole_leader_labels.py +0 -0
  180. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_issue_1144_transactional_hole_table.py +0 -0
  181. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_issue_1147_legibility_pair_aggregation.py +0 -0
  182. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_issue_676_polygonal_boss_declare.py +0 -0
  183. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_issue_915_dense_case.py +0 -0
  184. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_issue_916_pocket_leader.py +0 -0
  185. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_issue_955_height_contingency.py +0 -0
  186. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_label_provenance.py +0 -0
  187. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_layout.py +0 -0
  188. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_layout_cleanliness.py +0 -0
  189. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_layout_hypothesis.py +0 -0
  190. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_layout_property.py +0 -0
  191. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_leader_footprint.py +0 -0
  192. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_lint_box_cache.py +0 -0
  193. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_lint_reconciliation.py +0 -0
  194. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_lint_structural.py +0 -0
  195. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_linting.py +0 -0
  196. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_location_vocabulary.py +0 -0
  197. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_note_verb.py +0 -0
  198. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_object_aspects.py +0 -0
  199. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_occupancy_boxes.py +0 -0
  200. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_parameter_id.py +0 -0
  201. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_pitch_dim_footprint.py +0 -0
  202. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_pmi.py +0 -0
  203. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_pmi_datum_lowering.py +0 -0
  204. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_pmi_gtol_lowering.py +0 -0
  205. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_pmi_part21.py +0 -0
  206. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_pocket_pattern.py +0 -0
  207. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_principal_profile_classifier.py +0 -0
  208. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_private_test_attr_reads.py +0 -0
  209. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_refactor_golden.py +0 -0
  210. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_registry.py +0 -0
  211. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_render_seam.py +0 -0
  212. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_repack_geometry_seam.py +0 -0
  213. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_score.py +0 -0
  214. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_script_detail_parity.py +0 -0
  215. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_shared_box_memo.py +0 -0
  216. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_sheet_emit.py +0 -0
  217. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_sheet_gdt.py +0 -0
  218. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_sheet_identity_invariant.py +0 -0
  219. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_sheet_notes.py +0 -0
  220. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_sheet_of.py +0 -0
  221. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_sheet_section.py +0 -0
  222. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_sheet_tables.py +0 -0
  223. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_slot_pattern.py +0 -0
  224. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_soft_deprecation.py +0 -0
  225. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_solve_trace.py +0 -0
  226. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_suppression_ledger.py +0 -0
  227. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_suppression_marks.py +0 -0
  228. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_tolerances.py +0 -0
  229. {draftwright-0.4.4 → draftwright-0.4.6}/tests/test_witness_label_reconciliation.py +0 -0
@@ -1,6 +1,44 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## v0.4.6 — 2026-08-15
4
+
5
+ ### Changed
6
+
7
+ - **Geometry recognition is supplied by the standalone Apache-2.0
8
+ [`b123d-recognisers`](https://github.com/pzfreo/b123d-recognisers) package.** The cutover
9
+ pins the published stable release `v0.1.0` (built from commit
10
+ `9e622716c14d491729b5191aa6e5d8351c982a51`), removes the
11
+ duplicate embedded implementation, and keeps only identity-preserving
12
+ `draftwright.recognition` / `draftwright.score` compatibility re-exports until 0.6.0.
13
+ Draftwright continues to own the one-result build/lint cache, record→IR conversion, drafting
14
+ policy, placement, and critique. The package migration is manifest-pinned and golden-backed;
15
+ its sole normalization makes an exact dominant-axis tie deterministic across operating
16
+ systems without changing feature policy.
17
+
18
+ ### Fixed
19
+
20
+ - Hole-callout obstacle checks now account for the rendered leader shaft width and
21
+ tip-local arrowhead flare instead of treating the leader as a zero-width centreline
22
+ (#367). The analytical check remains geometry-only and does not over-reserve the
23
+ arrowhead's clearance along the whole shaft.
24
+ - Exact X/Z slanted flats retain their established lint-clean side-view callout after the
25
+ recogniser package's cross-platform semantic-axis normalization. The normalized recognition
26
+ record remains unchanged; `FlatFeature.presentation_axis` carries the downstream drawing-view
27
+ choice, preventing a leader/silhouette crossing without duplicating recognition.
28
+
29
+ ## v0.4.5 — 2026-08-15
30
+
31
+ **A focused technical-drawing correctness patch.** Cross-drilled hole locations now remain
32
+ complete and ordered, datum-starting pocket annotations avoid redundant dimensions, and repeated
33
+ equal-radius fillets collapse into one manufacturing callout.
34
+
35
+ ### Fixed
36
+
37
+ - Side-drilled hole heights that relocate around callout leaders now join the alternate
38
+ view's shared dimension ladder before placement, preserving every height and keeping
39
+ feature locations inside the overall dimension. Datum-starting open pockets no longer
40
+ print a redundant half-length centre offset, and equal-radius fillets across different
41
+ edge axes collapse into one counted ``n× R`` callout.
4
42
 
5
43
  ## v0.4.4 — 2026-08-15
6
44
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: draftwright
3
- Version: 0.4.4
3
+ Version: 0.4.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
@@ -684,6 +684,7 @@ Classifier: Programming Language :: Python :: 3.14
684
684
  Classifier: Topic :: Scientific/Engineering
685
685
  Requires-Python: <3.15,>=3.10
686
686
  Requires-Dist: antlr4-python3-runtime<4.10
687
+ Requires-Dist: b123d-recognisers==0.1.0
687
688
  Requires-Dist: build123d-drafting-helpers>=0.14.1
688
689
  Requires-Dist: build123d<0.11,>=0.9.0; python_full_version < '3.13'
689
690
  Requires-Dist: build123d<0.12,>=0.11; python_full_version >= '3.13'
@@ -967,17 +968,20 @@ or declared features converge on a `PartModel` IR, then planner-fed and
967
968
  sanctioned model-routed render intents feed shared placement, projection, and
968
969
  export. Coverage lint independently compares recognised geometry with the
969
970
  placed drawing, so an upstream omission cannot hide from verification. It
970
- builds on two libraries:
971
+ builds on three libraries:
971
972
 
972
973
  ```
973
974
  draftwright
975
+ └── b123d-recognisers — deterministic geometry recognition
974
976
  └── build123d-drafting-helpers — Dimension, Leader, HoleCallout, …
975
977
  └── build123d — CAD kernel
976
978
  ```
977
979
 
978
- It owns feature recognition (`recognition/`) and linting (`linting/`); annotation
979
- primitives (`Dimension`, `Leader`, etc.) live in `build123d-drafting-helpers` and can
980
- be used independently. The compiler is largely converged in production — turned
980
+ Geometry recognition lives in the Apache-2.0 `b123d-recognisers` package. Draftwright owns
981
+ the per-build recognition cache, record→IR conversion, drafting policy, and linting
982
+ (`linting/`); annotation primitives (`Dimension`, `Leader`, etc.) live in
983
+ `build123d-drafting-helpers` and can be used independently. The compiler is largely converged
984
+ in production — turned
981
985
  dims/lengths, centre marks, envelope, slots, holes (callouts/locations/grouping),
982
986
  the section A–A trigger, the prismatic step-ladder + rotational furniture, and PMI/GD&T
983
987
  are all on the IR. See
@@ -268,17 +268,20 @@ or declared features converge on a `PartModel` IR, then planner-fed and
268
268
  sanctioned model-routed render intents feed shared placement, projection, and
269
269
  export. Coverage lint independently compares recognised geometry with the
270
270
  placed drawing, so an upstream omission cannot hide from verification. It
271
- builds on two libraries:
271
+ builds on three libraries:
272
272
 
273
273
  ```
274
274
  draftwright
275
+ └── b123d-recognisers — deterministic geometry recognition
275
276
  └── build123d-drafting-helpers — Dimension, Leader, HoleCallout, …
276
277
  └── build123d — CAD kernel
277
278
  ```
278
279
 
279
- It owns feature recognition (`recognition/`) and linting (`linting/`); annotation
280
- primitives (`Dimension`, `Leader`, etc.) live in `build123d-drafting-helpers` and can
281
- be used independently. The compiler is largely converged in production — turned
280
+ Geometry recognition lives in the Apache-2.0 `b123d-recognisers` package. Draftwright owns
281
+ the per-build recognition cache, record→IR conversion, drafting policy, and linting
282
+ (`linting/`); annotation primitives (`Dimension`, `Leader`, etc.) live in
283
+ `build123d-drafting-helpers` and can be used independently. The compiler is largely converged
284
+ in production — turned
282
285
  dims/lengths, centre marks, envelope, slots, holes (callouts/locations/grouping),
283
286
  the section A–A trigger, the prismatic step-ladder + rotational furniture, and PMI/GD&T
284
287
  are all on the IR. See
@@ -25,15 +25,15 @@ architecture** table; open retired or superseded records only for design history
25
25
  | [0004](0004-compose-then-pack-view-blocks.md) | Compose-then-pack: views as blocks carrying their annotation footprint | Compose each view with its annotation footprint, then pack fixed-topology blocks. | Accepted | `test_layout.py`, `test_layout_cleanliness.py`, `test_refactor_golden.py` |
26
26
  | [0005](0005-pipeline-architecture-and-state-ownership.md) | Compiler-pipeline module boundaries and single-owner build state | Give compiler stages explicit module homes and build-time state explicit owners. | Accepted; implemented; compatibility aliases tracked by #720 | `test_import_boundaries.py`, `test_drawing_encapsulation.py`, `test_registry.py` |
27
27
  | [0006](0006-deterministic-layout-via-bundled-fonts.md) | Deterministic cross-platform layout via bundled, path-pinned fonts | Pin bundled font files so text measurement and layout are cross-platform deterministic. | Accepted | `test_refactor_golden.py`, `test_layout_cleanliness.py` |
28
- | [0007](0007-own-recognition-and-linting.md) | draftwright owns feature recognition and linting; helpers becomes the rendering library | draftwright owns recognition and linting; helpers remains a rendering library. | Accepted | `test_import_boundaries.py`, `test_recognition.py`, `test_linting.py` |
28
+ | [0007](0007-own-recognition-and-linting.md) | draftwright owns feature recognition and linting; helpers becomes the rendering library | Draftwright owns linting, recognition lifecycle, IR conversion, and drafting policy; `b123d-recognisers` owns geometry recognition; helpers remains the rendering library. | Accepted; extraction deployed by Amendment 2 | `test_import_boundaries.py`, `test_external_recognition_boundary.py`, `test_linting.py` |
29
29
  | [0010](0010-annotation-provenance-seam.md) | Annotation provenance: record intent → annotation once, at the render seam | Record annotation provenance once at the render/add seam. | Accepted; landed | `test_render_seam.py`, `test_registry.py` |
30
30
  | [0011](0011-ir-as-public-input.md) | The IR as a public input: declare features, don't only detect them | Accept the feature IR as public input through declarations and the `Sheet` façade. | Accepted; core landed; #62/#462/#495 remain | `test_declare.py`, `test_object_aspects.py`, `test_sheet_gdt.py` |
31
31
  | [0012](0012-edits-as-pinned-priority-candidates-in-the-global-solve.md) | User annotation edits are pinned, priority-ranked corridor candidates | Drain recorded semantic edits through corridor placement with pin and priority. | Accepted; partially landed; full recomposition/parity remains #426/#707 (#661 detail views landed) | `test_make_drawing.py`, `test_sheet_emit.py` |
32
- | [0013](0013-uniform-recognition-and-shared-package.md) | A uniform recogniser/feature contract (with `b123d-recognisers` as its deferred shared deployment) | Enforce a uniform geometry-only recogniser contract internally; defer package extraction until there is a second consumer. | Accepted; Phase 1 in progress; extraction deferred | `test_recogniser_contract.py`, `test_import_boundaries.py` |
32
+ | [0013](0013-uniform-recognition-and-shared-package.md) | A uniform recogniser/feature contract and shared `b123d-recognisers` deployment | Enforce the geometry-only contract in the shared package and consume it through Draftwright's record→IR boundary. | Accepted; Phases 1–2 deployed | `test_external_recognition_boundary.py`, `test_detect_registry.py`, package semantic goldens |
33
33
  | [0014](0014-collect-then-solve-annotation-placement.md) | Collect-then-solve annotation placement (as built) | Collect, select, assign, and deterministically solve annotations per corridor before rendering. | Accepted; supersedes 0009 | `test_carve_free_position_callers.py`, `test_strip_layout.py`, `test_layout_property.py`, `test_import_boundaries.py` |
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
- | [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` |
36
+ | [0017](0017-recognition-inventory-correspondence-and-measurement-provenance.md) | One recognition result per run; correspondence is evidence-gated | Produce one external immutable recognition result held by Draftwright's per-run cache; require vertical-slice evidence before generalising correspondence, identity, requirements, outcomes, or reconciliation. | Accepted; external cache ownership clarified, extensions gated by #1018 | `test_external_recognition_boundary.py`, `test_recognition_manifest.py`, `test_declared_recognition_gate.py` |
37
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 |
38
38
 
39
39
  ## Historical records
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "draftwright"
7
- version = "0.4.4"
7
+ version = "0.4.6"
8
8
  description = "Automated technical-drawing generation for build123d"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -18,6 +18,7 @@ dependencies = [
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
20
  "build123d-drafting-helpers>=0.14.1",
21
+ "b123d-recognisers==0.1.0",
21
22
  "numpy>=1.24",
22
23
  # PNG raster export (SVG→PDF→PNG). Both permissively licensed (Pillow HPND; pypdfium2
23
24
  # Apache-2.0 wrapping Google PDFium BSD-3) and ship pre-built wheels with NO native system
@@ -163,6 +164,7 @@ dev = [
163
164
  "pytest-timeout>=2",
164
165
  "pytest-xdist>=3",
165
166
  "ruff>=0.4",
167
+ "tomli>=2; python_version < '3.11'",
166
168
  ]
167
169
 
168
170
  [tool.codespell]
@@ -25,8 +25,9 @@ from types import SimpleNamespace
25
25
  from typing import TYPE_CHECKING, Literal
26
26
 
27
27
  if TYPE_CHECKING:
28
+ from b123d_recognisers import RecognitionResult, TurnedProfile
29
+
28
30
  from draftwright.compose import StripDepths
29
- from draftwright.recognition import RecognitionResult, TurnedProfile
30
31
 
31
32
  from build123d import Align, BoundBox, Compound, Edge, Location, Mode, Shape, Text, Vector
32
33
  from build123d_drafting.helpers import (
@@ -327,6 +327,94 @@ def _segment_clips_box(p, q, box, pad=0.0) -> bool:
327
327
  return _segment_clip_extent(p, q, box, pad=pad) is not None
328
328
 
329
329
 
330
+ def _convex_polygon_overlaps_box(points, box) -> bool:
331
+ """Whether a convex page-plane polygon has positive-area overlap with an AABB.
332
+
333
+ This is the small separating-axis primitive used by
334
+ :func:`_leader_ink_crosses_box`. Keeping it here makes the rendered-ink model pure
335
+ rectangle/vector maths: placement does not build temporary OCC geometry merely to ask
336
+ whether a candidate is clear (ADRs 0004/0014).
337
+ """
338
+ corners = (
339
+ (box[0], box[1]),
340
+ (box[2], box[1]),
341
+ (box[2], box[3]),
342
+ (box[0], box[3]),
343
+ )
344
+ axes = [(1.0, 0.0), (0.0, 1.0)]
345
+ axes.extend(
346
+ (-(second[1] - first[1]), second[0] - first[0])
347
+ for first, second in zip(points, (*points[1:], points[0]), strict=True)
348
+ )
349
+ for ax, ay in axes:
350
+ if abs(ax) + abs(ay) <= 1e-12:
351
+ continue
352
+ polygon_projection = [x * ax + y * ay for x, y in points]
353
+ box_projection = [x * ax + y * ay for x, y in corners]
354
+ if max(polygon_projection) <= min(box_projection) or max(box_projection) <= min(
355
+ polygon_projection
356
+ ):
357
+ return False
358
+ return True
359
+
360
+
361
+ def _leader_ink_crosses_box(
362
+ tip,
363
+ elbow,
364
+ box,
365
+ *,
366
+ arrow_length: float,
367
+ line_width: float,
368
+ ) -> bool:
369
+ """Whether a rendered leader's tip-to-elbow ink overlaps *box*.
370
+
371
+ A leader is not a zero-width segment. Its shaft is a swept rectangle of
372
+ ``line_width`` and each supported arrowhead style is contained by the triangle from
373
+ the tip to a base ``arrow_length`` along the shaft, with the helper's
374
+ ``arrow_length / 3`` lateral flare. Testing those two local convex footprints keeps
375
+ the arrow clearance near the tip; inflating the entire shaft by the arrow size would
376
+ reject genuinely clear obstacles near the elbow (#367).
377
+
378
+ Touching boundaries are clear, matching :func:`_boxes_overlap` and the placement-side
379
+ semantics of :func:`_segment_crosses_box`.
380
+ """
381
+ dx, dy = float(elbow[0]) - float(tip[0]), float(elbow[1]) - float(tip[1])
382
+ length = math.hypot(dx, dy)
383
+ half_line = max(0.0, float(line_width)) / 2.0
384
+ head_length = max(0.0, float(arrow_length))
385
+
386
+ if length <= 1e-12:
387
+ half = max(half_line, head_length / 3.0)
388
+ point_box = (tip[0] - half, tip[1] - half, tip[0] + half, tip[1] + half)
389
+ return _boxes_overlap(point_box, box)
390
+
391
+ ux, uy = dx / length, dy / length
392
+ vx, vy = -uy, ux
393
+
394
+ if half_line > 0.0:
395
+ shaft = (
396
+ (tip[0] + vx * half_line, tip[1] + vy * half_line),
397
+ (elbow[0] + vx * half_line, elbow[1] + vy * half_line),
398
+ (elbow[0] - vx * half_line, elbow[1] - vy * half_line),
399
+ (tip[0] - vx * half_line, tip[1] - vy * half_line),
400
+ )
401
+ if _convex_polygon_overlaps_box(shaft, box):
402
+ return True
403
+
404
+ if head_length > 0.0:
405
+ half_head = head_length / 3.0
406
+ base_x, base_y = tip[0] + ux * head_length, tip[1] + uy * head_length
407
+ arrow = (
408
+ (tip[0], tip[1]),
409
+ (base_x + vx * half_head, base_y + vy * half_head),
410
+ (base_x - vx * half_head, base_y - vy * half_head),
411
+ )
412
+ if _convex_polygon_overlaps_box(arrow, box):
413
+ return True
414
+
415
+ return False
416
+
417
+
330
418
  def _segments_cross_or_overlap(a1, a2, b1, b2) -> bool:
331
419
  """Whether two page-plane segments cross or overlap beyond a shared endpoint.
332
420
 
@@ -17,6 +17,14 @@ from collections.abc import Callable
17
17
  from dataclasses import dataclass, replace
18
18
  from typing import cast
19
19
 
20
+ from b123d_recognisers import (
21
+ RecognitionResult,
22
+ TurnedProfile,
23
+ TurnedStep,
24
+ analyse_cylinders,
25
+ build_recognition_result,
26
+ full_cylinders,
27
+ )
20
28
  from build123d import Compound, Shape
21
29
  from build123d_drafting.helpers import draft_preset
22
30
  from OCP.IFSelect import IFSelect_ReturnStatus
@@ -47,14 +55,6 @@ from draftwright.compose import (
47
55
  from draftwright.model import build_part_model
48
56
  from draftwright.model.ir import Datum, PartModel, StepFeature, StepLevelFeature
49
57
  from draftwright.model.planner import plan_dimensions
50
- from draftwright.recognition import (
51
- RecognitionResult,
52
- TurnedProfile,
53
- TurnedStep,
54
- analyse_cylinders,
55
- build_recognition_result,
56
- full_cylinders,
57
- )
58
58
 
59
59
  _log = logging.getLogger(__name__)
60
60
 
@@ -1759,16 +1759,16 @@ def _fillet_label(radius_text, count) -> str:
1759
1759
 
1760
1760
  def render_fillets(dwg, plan, a, *, ctx, only=None) -> int:
1761
1761
  """Fillet radius callouts (#561): a leader from an external edge fillet to its
1762
- ``R{radius}`` label — the arc analog of :func:`render_chamfers`. Equal-radius fillets on
1763
- the same edge axis share ONE ``n× R`` callout (#561 acceptance), placed in the view
1764
- normal to the rounded edge, led diagonally out of the corner into clear margin, and
1762
+ ``R{radius}`` label — the arc analog of :func:`render_chamfers`. Equal-radius fillets
1763
+ share ONE ``n× R`` callout (#561 acceptance), placed in the view normal to a
1764
+ representative rounded edge, led diagonally out of the corner into clear margin, and
1765
1765
  dropped (lint, not silently) if it would overprint placed geometry. Returns the count.
1766
1766
 
1767
1767
  Planner-fed (#725 / #698): the radius VALUE + its tolerance come from the planner's
1768
1768
  ``DimParameter`` (as in ``render_chamfers``), bound explicitly by ``(role, kind)``,
1769
1769
  never positionally — formatting ``fl.radius`` directly dropped an authored tolerance
1770
1770
  (the #629 class). The equal-radius ``n× R`` COLLAPSE stays render-side (grouping by
1771
- ``(axis, value)`` here, as before) — planner-side grouping is a structural change,
1771
+ radius here) — planner-side grouping is a structural change,
1772
1772
  explicitly out of scope for this migration (#698). Where grouped members' authored
1773
1773
  tolerances differ, the first (member-order) authored one wins — the documented
1774
1774
  ``render_diameters`` shared-ø precedent. ``g.view`` is safe: a FilletFeature's frame
@@ -1785,9 +1785,9 @@ def render_fillets(dwg, plan, a, *, ctx, only=None) -> int:
1785
1785
  )
1786
1786
  if pd is None:
1787
1787
  continue
1788
- collapse.setdefault((g.facts.axis, round(pd.value, 3)), []).append((g, pd))
1788
+ collapse.setdefault(round(pd.value, 3), []).append((g, pd))
1789
1789
  jobs = []
1790
- for gi, ((axis, _radius), members) in enumerate(sorted(collapse.items())):
1790
+ for gi, (_radius, members) in enumerate(sorted(collapse.items())):
1791
1791
  if only is not None:
1792
1792
  # #426 Ph2b subset (finalize): filter members AFTER the collapse is enumerated so
1793
1793
  # gi stays the full-drawing group index — a survivor keeps its m_fillet name even
@@ -1795,10 +1795,15 @@ def render_fillets(dwg, plan, a, *, ctx, only=None) -> int:
1795
1795
  members = [gp for gp in members if gp[0].ref in only]
1796
1796
  if not members:
1797
1797
  continue
1798
- # One grouped ``n× R`` callout, but its leader may anchor at ANY of the equal fillets
1799
- # _corner_candidates tries each corner (nearest-clear first) so the group is not
1800
- # dropped just because its first corner leads into an occupied region.
1801
- ordered = sorted(members, key=lambda gp: gp[0].facts.frame.origin)
1798
+ # Point the leader at one coherent visible set. Members on other edge axes still
1799
+ # contribute to the printed count and semantic measurements, but mixing their 3-D
1800
+ # origins into this view could point at unrelated projected corners. Prefer the
1801
+ # axis with the most members; ties are deterministic.
1802
+ by_axis: dict[str, list] = {}
1803
+ for gp in members:
1804
+ by_axis.setdefault(gp[0].facts.axis, []).append(gp)
1805
+ axis, visible = min(by_axis.items(), key=lambda item: (-len(item[1]), item[0]))
1806
+ ordered = sorted(visible, key=lambda gp: gp[0].facts.frame.origin)
1802
1807
  view = ordered[0][0].view
1803
1808
  vb = dwg.view_bounds(view)
1804
1809
  if vb is None:
@@ -1812,7 +1817,7 @@ def render_fillets(dwg, plan, a, *, ctx, only=None) -> int:
1812
1817
  f"m_fillet_{axis}{gi}",
1813
1818
  view,
1814
1819
  vb,
1815
- _fillet_label(members[0][1].value_text, len(ordered)) + _tol_suffix(tol, draft),
1820
+ _fillet_label(members[0][1].value_text, len(members)) + _tol_suffix(tol, draft),
1816
1821
  _corner_candidates(
1817
1822
  dwg,
1818
1823
  view,
@@ -1823,7 +1828,7 @@ def render_fillets(dwg, plan, a, *, ctx, only=None) -> int:
1823
1828
  ),
1824
1829
  # One `n× R` callout stands for EVERY collapsed member, so it draws all of
1825
1830
  # their radii — the tuple storage exists for exactly this (#1002).
1826
- tuple(pd.id for _, pd in ordered),
1831
+ tuple(pd.id for _, pd in members),
1827
1832
  )
1828
1833
  )
1829
1834
  return _leader_callout_pass(dwg, a, jobs, noun="fillet", drop_code="fillet_dropped", ctx=ctx)
@@ -1876,6 +1881,7 @@ def render_flats(dwg, plan, a, *, ctx, only=None) -> int:
1876
1881
  collapse.setdefault(
1877
1882
  (
1878
1883
  g.facts.axis,
1884
+ g.facts.presentation_axis,
1879
1885
  g.facts.axis_direction,
1880
1886
  g.facts.axis_line,
1881
1887
  g.facts.stock_span,
@@ -1884,7 +1890,7 @@ def render_flats(dwg, plan, a, *, ctx, only=None) -> int:
1884
1890
  [],
1885
1891
  ).append((g, pd))
1886
1892
  jobs = []
1887
- for gi, ((axis, _direction, _line, _span, _across), members) in enumerate(
1893
+ for gi, ((_axis, presentation_axis, _direction, _line, _span, _across), members) in enumerate(
1888
1894
  sorted(collapse.items())
1889
1895
  ):
1890
1896
  if only is not None:
@@ -1915,7 +1921,7 @@ def render_flats(dwg, plan, a, *, ctx, only=None) -> int:
1915
1921
  )
1916
1922
  jobs.append(
1917
1923
  (
1918
- f"m_flat_{axis}{gi}",
1924
+ f"m_flat_{presentation_axis}{gi}",
1919
1925
  view,
1920
1926
  vb,
1921
1927
  _flat_label(members[0][1].value_text, _tol_suffix(tol, draft)),
@@ -33,7 +33,7 @@ from draftwright._core import (
33
33
  _iso_bbox,
34
34
  _log,
35
35
  )
36
- from draftwright._geometry import plane_axes
36
+ from draftwright._geometry import _leader_ink_crosses_box, plane_axes
37
37
  from draftwright.annotations._common import (
38
38
  CROSSABLE_TYPES,
39
39
  CorridorCandidate,
@@ -42,17 +42,18 @@ from draftwright.annotations._common import (
42
42
  _geom_box,
43
43
  _hole_location_coverage_fact,
44
44
  _same_location_ordinate,
45
- _segment_crosses_box,
46
45
  _with_hole_center_coverage,
47
46
  _with_hole_location_coverage,
48
47
  box_within_page_and_clear,
49
48
  carve_free_position,
50
49
  carve_free_segments,
51
50
  clear_label_of_centerlines,
51
+ corridor_blockers,
52
52
  dim_footprint,
53
53
  leader_footprint,
54
54
  place_strip_candidates,
55
55
  register_corridor,
56
+ strip_free_span,
56
57
  strip_obstacles,
57
58
  )
58
59
  from draftwright.annotations.from_model import (
@@ -967,6 +968,42 @@ def _locate_along_z(dwg, ctx, a: Analysis, off):
967
968
  strip, view, p_lo, p_hi, edge = primary
968
969
  primary_cand = _zc(view, p_lo, p_hi, edge)
969
970
 
971
+ # Choose the alternate BEFORE the shared corridor solve when the natural
972
+ # witness corridor is already known to cross a placed leader. A post-drain
973
+ # relocation cannot join the alternate view's existing ladder, so it used to
974
+ # land outside the overall height and cross its witness line. The inner-tier
975
+ # footprint is decisive: moving a right-side dimension farther out only grows
976
+ # the corridor back to the view and therefore cannot clear an existing blocker.
977
+ if strip is not None and alternates:
978
+ _lo, _hi, inner = strip_free_span(strip)
979
+ blocked = _box_hits(_geom_box(primary_cand[1](inner)), corridor_blockers(dwg, view))
980
+ if blocked:
981
+ alt_strip, alt_view, alt_p_lo, alt_p_hi, alt_edge = alternates[0]
982
+ alt_cand = _zc(alt_view, alt_p_lo, alt_p_hi, alt_edge)
983
+ alt_blocked = alt_strip is None
984
+ if alt_strip is not None:
985
+ _alo, _ahi, alt_inner = strip_free_span(alt_strip)
986
+ alt_blocked = _box_hits(
987
+ _geom_box(alt_cand[1](alt_inner)),
988
+ corridor_blockers(dwg, alt_view),
989
+ )
990
+ # Do not pre-route into a corridor that cannot hold its existing
991
+ # shared ladder plus this candidate. In that case the historical
992
+ # alternate/force path remains the honest policy-B fallback.
993
+ existing = len(
994
+ ctx.corridor_batch.get((alt_view, "right"), {}).get("cands", ())
995
+ )
996
+ pad = tier + alt_strip.spacing
997
+ usable = max(0.0, _ahi - _alo - tier)
998
+ capacity = int(usable / pad) + 1
999
+ alt_blocked = alt_blocked or capacity < existing + 1
1000
+ if not alt_blocked:
1001
+ original = primary
1002
+ primary = alternates[0]
1003
+ alternates = [original, *alternates[1:]]
1004
+ strip, view, p_lo, p_hi, edge = primary
1005
+ primary_cand = alt_cand
1006
+
970
1007
  def _fallback(
971
1008
  _nm,
972
1009
  _primary=primary,
@@ -974,10 +1011,14 @@ def _locate_along_z(dwg, ctx, a: Analysis, off):
974
1011
  _cand=primary_cand,
975
1012
  _feature_map=_zf,
976
1013
  _measurement_map=_zm,
1014
+ _candidate_factory=_zc,
977
1015
  _zo=zo,
978
1016
  ):
979
1017
  for alt_strip, alt_view, alt_p_lo, alt_p_hi, alt_edge in _alts:
980
- alt = _zc(alt_view, alt_p_lo, alt_p_hi, alt_edge)
1018
+ # Capture this loop member's factory. Referring to `_zc` directly
1019
+ # late-bound every callback to the final height, so two dropped hole
1020
+ # heights retried the last one twice and silently lost the first.
1021
+ alt = _candidate_factory(alt_view, alt_p_lo, alt_p_hi, alt_edge)
981
1022
  if not _off_axis_emit(
982
1023
  dwg,
983
1024
  tier,
@@ -1843,15 +1884,25 @@ def _needs_section(feat: HoleFeature | PatternFeature):
1843
1884
  return bore.cbore is not None or bore.spotface is not None or not bore.through
1844
1885
 
1845
1886
 
1846
- def _leader_hits(leader, tip, elbow, side, obstacles):
1887
+ def _leader_hits(leader, tip, elbow, side, obstacles, draft):
1847
1888
  """True when *leader*'s rendered footprint truly overlaps any *obstacles* box — split into
1848
- the diagonal tip→elbow SHAFT (checked precisely via `_segment_crosses_box`, since its AABB
1849
- over-claims the empty triangle it doesn't occupy — #305) and the elbow→label shelf+text
1850
- (axis-aligned, so the coarse AABB check there is already exact). Promoted (#638; pure)."""
1889
+ the diagonal tip→elbow SHAFT + ARROWHEAD (checked against their analytical rendered ink,
1890
+ since the composite AABB over-claims the empty triangle it doesn't occupy — #305/#367) and
1891
+ the elbow→label shelf+text (axis-aligned, so the coarse AABB check there is already exact).
1892
+ Promoted (#638; pure)."""
1851
1893
  full_box = _geom_box(leader)
1852
1894
  if full_box is None or not _box_hits(full_box, obstacles):
1853
1895
  return False # fast reject: nowhere near any obstacle
1854
- if any(_segment_crosses_box(tip, elbow, o) for o in obstacles):
1896
+ if any(
1897
+ _leader_ink_crosses_box(
1898
+ tip,
1899
+ elbow,
1900
+ obstacle,
1901
+ arrow_length=draft.arrow_length,
1902
+ line_width=draft.line_width,
1903
+ )
1904
+ for obstacle in obstacles
1905
+ ):
1855
1906
  return True
1856
1907
  label_box = (
1857
1908
  (elbow[0], full_box[1], full_box[2], full_box[3])
@@ -2391,7 +2442,7 @@ def _place_queue(
2391
2442
  continue
2392
2443
  y = final_y[tid]
2393
2444
  leader, tip, elbow = _build_leader_at(s, edge, side, y, to_page, elbow_dx, draft, a.SCALE)
2394
- if _leader_hits(leader, tip, elbow, side, occupied):
2445
+ if _leader_hits(leader, tip, elbow, side, occupied, draft):
2395
2446
  crossing.append((s, y, leader))
2396
2447
  else:
2397
2448
  placed.append((s, y, leader))
@@ -20,7 +20,7 @@ from dataclasses import field as dataclasses_field
20
20
  from typing import TYPE_CHECKING, Any, NamedTuple
21
21
 
22
22
  if TYPE_CHECKING:
23
- from draftwright.recognition import RecognitionResult
23
+ from b123d_recognisers import RecognitionResult
24
24
 
25
25
  # PEP 702 @deprecated. A `sys.version_info` guard (not try/except) so the type checker,
26
26
  # which targets the 3.10 floor, resolves the backport branch instead of `warnings.deprecated`
@@ -30,6 +30,7 @@ if sys.version_info >= (3, 13):
30
30
  else:
31
31
  from typing_extensions import deprecated
32
32
 
33
+ from b123d_recognisers import analyse_cylinders
33
34
  from build123d import (
34
35
  Color,
35
36
  ExportSVG,
@@ -98,7 +99,7 @@ from draftwright.linting.quality import quality_components
98
99
  from draftwright.projection import (
99
100
  project_view_geometry,
100
101
  )
101
- from draftwright.recognition import analyse_cylinders, build_recognition_result
102
+ from draftwright.recognition_cache import RecognitionCache
102
103
  from draftwright.registry import AnnotationRegistry
103
104
  from draftwright.repair import repair_drawing
104
105
 
@@ -320,7 +321,7 @@ class BuildState:
320
321
  """
321
322
 
322
323
  analysis: Analysis | None = None
323
- recognition: RecognitionResult | None = None
324
+ recognition_cache: RecognitionCache = dataclasses_field(default_factory=RecognitionCache)
324
325
  part_model: object | None = None
325
326
  view_edge_cache: dict = dataclasses_field(default_factory=dict)
326
327
  ann_box_cache: dict = dataclasses_field(default_factory=dict)
@@ -335,6 +336,16 @@ class BuildState:
335
336
  #: suppression rule produced four issue reports before anyone found the rule (#997).
336
337
  omissions: tuple = ()
337
338
 
339
+ @property
340
+ def recognition(self) -> RecognitionResult | None:
341
+ """The immutable result held by Draftwright's consumer-owned lifecycle cache."""
342
+
343
+ return self.recognition_cache.result
344
+
345
+ @recognition.setter
346
+ def recognition(self, value: RecognitionResult | None) -> None:
347
+ self.recognition_cache.result = value
348
+
338
349
  def clear_geometry_caches(self) -> None:
339
350
  """The one invalidation seam (finalize rollback): view edges + annotation
340
351
  boxes together — a rolled-back drawing must re-measure everything."""
@@ -353,9 +364,7 @@ class BuildState:
353
364
  On a detected build ``recognition`` is already filled by the builder, so this returns
354
365
  it and recognises nothing.
355
366
  """
356
- if self.recognition is None:
357
- self.recognition = build_recognition_result(part)
358
- return self.recognition
367
+ return self.recognition_cache.ensure(part)
359
368
 
360
369
 
361
370
  class Drawing:
@@ -5,8 +5,9 @@ from __future__ import annotations
5
5
  from dataclasses import dataclass
6
6
  from typing import Literal
7
7
 
8
+ from b123d_recognisers import RecognitionResult, has_multi_axis_plates
9
+
8
10
  from draftwright.linting.issues import LintIssue
9
- from draftwright.recognition import RecognitionResult, has_multi_axis_plates
10
11
 
11
12
  ChannelRequirementState = Literal["placed", "suppressed", "dropped", "missing", "unverifiable"]
12
13