draftwright 0.4.5__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.5 → draftwright-0.4.6}/CHANGELOG.md +25 -1
  2. {draftwright-0.4.5 → draftwright-0.4.6}/PKG-INFO +9 -5
  3. {draftwright-0.4.5 → draftwright-0.4.6}/README.md +7 -4
  4. {draftwright-0.4.5 → draftwright-0.4.6}/docs/adr/README.md +3 -3
  5. {draftwright-0.4.5 → draftwright-0.4.6}/pyproject.toml +3 -1
  6. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/_core.py +2 -1
  7. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/_geometry.py +88 -0
  8. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/analysis.py +8 -8
  9. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/annotations/from_model.py +3 -2
  10. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/annotations/holes.py +17 -8
  11. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/drawing.py +15 -6
  12. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/linting/channel_coverage.py +2 -1
  13. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/linting/coverage.py +15 -15
  14. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/linting/flat_coverage.py +2 -1
  15. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/linting/hole_coverage.py +2 -1
  16. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/linting/polygonal_stock_coverage.py +2 -1
  17. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/linting/profiled_bore_coverage.py +2 -1
  18. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/linting/slot_coverage.py +2 -1
  19. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/model/compiled.py +8 -1
  20. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/model/declare.py +8 -9
  21. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/model/detect.py +35 -34
  22. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/model/ir.py +19 -1
  23. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/model/planner.py +3 -0
  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.5 → draftwright-0.4.6}/tests/test_declare.py +13 -17
  28. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_declared_recognition_gate.py +9 -9
  29. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_detect_once.py +2 -2
  30. {draftwright-0.4.5 → 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.5 → draftwright-0.4.6}/tests/test_fillets_adjacency.py +1 -2
  33. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_flat_completeness.py +2 -2
  34. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_flat_stock_identity.py +21 -5
  35. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_flat_stock_opposition.py +1 -2
  36. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_grid_lattice_convention.py +3 -3
  37. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_import_boundaries.py +1 -0
  38. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_issue_1058_wheel_profile.py +23 -23
  39. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_issue_1073_cross_body_patterns.py +5 -5
  40. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_issue_1082_polygonal_stock.py +5 -5
  41. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_issue_1143_hole_completeness.py +7 -7
  42. {draftwright-0.4.5 → 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.5 → draftwright-0.4.6}/tests/test_issue_676_polygonal_boss.py +2 -2
  45. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_issue_885_prismatic_coverage.py +1 -1
  46. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_issue_909_sloped_profile.py +1 -1
  47. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_issue_917_open_channel.py +1 -1
  48. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_issue_958_cross_solid_recognition.py +4 -4
  49. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_make_drawing.py +34 -46
  50. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_part_model.py +1 -1
  51. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_pocket_pattern_recognition.py +9 -9
  52. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_private_test_imports.py +5 -0
  53. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_quality_components.py +2 -1
  54. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_recogniser_contract.py +3 -4
  55. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_recognition.py +28 -30
  56. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_recognition_manifest.py +14 -14
  57. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_recognition_result.py +9 -8
  58. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_slanted_blind_step.py +3 -3
  59. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_slot_completeness.py +1 -1
  60. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_slot_pattern_recognition.py +6 -6
  61. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_turned_steps.py +1 -2
  62. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_workflows.py +14 -0
  63. draftwright-0.4.5/src/draftwright/recognition/__init__.py +0 -205
  64. draftwright-0.4.5/src/draftwright/recognition/_features.py +0 -1164
  65. draftwright-0.4.5/src/draftwright/recognition/_record.py +0 -28
  66. draftwright-0.4.5/src/draftwright/recognition/chamfers.py +0 -196
  67. draftwright-0.4.5/src/draftwright/recognition/countersinks.py +0 -206
  68. draftwright-0.4.5/src/draftwright/recognition/fillets.py +0 -183
  69. draftwright-0.4.5/src/draftwright/recognition/flats.py +0 -275
  70. draftwright-0.4.5/src/draftwright/recognition/grooves.py +0 -145
  71. draftwright-0.4.5/src/draftwright/recognition/levels.py +0 -346
  72. draftwright-0.4.5/src/draftwright/recognition/pads.py +0 -148
  73. draftwright-0.4.5/src/draftwright/recognition/plates.py +0 -154
  74. draftwright-0.4.5/src/draftwright/recognition/polygonal_bosses.py +0 -342
  75. draftwright-0.4.5/src/draftwright/recognition/profiled_bores.py +0 -368
  76. draftwright-0.4.5/src/draftwright/recognition/repeating_profiles.py +0 -393
  77. draftwright-0.4.5/src/draftwright/recognition/result.py +0 -293
  78. draftwright-0.4.5/src/draftwright/recognition/slots.py +0 -1465
  79. draftwright-0.4.5/src/draftwright/recognition/turned.py +0 -202
  80. draftwright-0.4.5/src/draftwright/score.py +0 -76
  81. {draftwright-0.4.5 → draftwright-0.4.6}/.gitignore +0 -0
  82. {draftwright-0.4.5 → draftwright-0.4.6}/LICENSE +0 -0
  83. {draftwright-0.4.5 → draftwright-0.4.6}/skills/SKILL.md +0 -0
  84. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/__init__.py +0 -0
  85. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/_pmi_part21.py +0 -0
  86. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/_warnings.py +0 -0
  87. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/annotate.py +0 -0
  88. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/annotations/__init__.py +0 -0
  89. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/annotations/_common.py +0 -0
  90. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/annotations/balloons.py +0 -0
  91. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/annotations/gears.py +0 -0
  92. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/annotations/orchestrator.py +0 -0
  93. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/annotations/sections.py +0 -0
  94. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/audit.py +0 -0
  95. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/builder.py +0 -0
  96. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/cli.py +0 -0
  97. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/compose.py +0 -0
  98. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/export.py +0 -0
  99. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/fits.py +0 -0
  100. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
  101. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
  102. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
  103. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
  104. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/fonts/__init__.py +0 -0
  105. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/intents.py +0 -0
  106. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/layout.py +0 -0
  107. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/linting/__init__.py +0 -0
  108. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/linting/gear_coverage.py +0 -0
  109. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/linting/issues.py +0 -0
  110. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/linting/pmi_coverage.py +0 -0
  111. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/linting/quality.py +0 -0
  112. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/linting/structural.py +0 -0
  113. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/linting/suggest.py +0 -0
  114. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/make_drawing.py +0 -0
  115. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/model/__init__.py +0 -0
  116. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/model/callout.py +0 -0
  117. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/pmi.py +0 -0
  118. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/projection.py +0 -0
  119. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/py.typed +0 -0
  120. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/registry.py +0 -0
  121. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/repair.py +0 -0
  122. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/sheet.py +0 -0
  123. {draftwright-0.4.5 → draftwright-0.4.6}/src/draftwright/sheet_emit.py +0 -0
  124. {draftwright-0.4.5 → draftwright-0.4.6}/tests/_kernel.py +0 -0
  125. {draftwright-0.4.5 → draftwright-0.4.6}/tests/_layout_sig.py +0 -0
  126. {draftwright-0.4.5 → draftwright-0.4.6}/tests/conftest.py +0 -0
  127. {draftwright-0.4.5 → draftwright-0.4.6}/tests/fixtures/grm03_thumbwheel_drive_screw.step +0 -0
  128. {draftwright-0.4.5 → draftwright-0.4.6}/tests/fixtures/if_step_flat_across_cylinder.step +0 -0
  129. {draftwright-0.4.5 → draftwright-0.4.6}/tests/fixtures/issue_1058_wheel_rh.step +0 -0
  130. {draftwright-0.4.5 → draftwright-0.4.6}/tests/fixtures/issue_909_basic_part_design_017_body.step +0 -0
  131. {draftwright-0.4.5 → draftwright-0.4.6}/tests/fixtures/issue_915_case_study_2.step +0 -0
  132. {draftwright-0.4.5 → draftwright-0.4.6}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
  133. {draftwright-0.4.5 → draftwright-0.4.6}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
  134. {draftwright-0.4.5 → draftwright-0.4.6}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
  135. {draftwright-0.4.5 → draftwright-0.4.6}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
  136. {draftwright-0.4.5 → draftwright-0.4.6}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
  137. {draftwright-0.4.5 → draftwright-0.4.6}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
  138. {draftwright-0.4.5 → draftwright-0.4.6}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
  139. {draftwright-0.4.5 → draftwright-0.4.6}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
  140. {draftwright-0.4.5 → draftwright-0.4.6}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
  141. {draftwright-0.4.5 → draftwright-0.4.6}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
  142. {draftwright-0.4.5 → draftwright-0.4.6}/tests/fixtures/tuner_jig_blind_obround_pockets.step +0 -0
  143. {draftwright-0.4.5 → draftwright-0.4.6}/tests/refactor_golden/bracket_section.json +0 -0
  144. {draftwright-0.4.5 → draftwright-0.4.6}/tests/refactor_golden/centered_rebate.json +0 -0
  145. {draftwright-0.4.5 → draftwright-0.4.6}/tests/refactor_golden/chamfered.json +0 -0
  146. {draftwright-0.4.5 → draftwright-0.4.6}/tests/refactor_golden/filleted.json +0 -0
  147. {draftwright-0.4.5 → draftwright-0.4.6}/tests/refactor_golden/flange_dense.json +0 -0
  148. {draftwright-0.4.5 → draftwright-0.4.6}/tests/refactor_golden/grid_plate.json +0 -0
  149. {draftwright-0.4.5 → draftwright-0.4.6}/tests/refactor_golden/grooved_shaft.json +0 -0
  150. {draftwright-0.4.5 → draftwright-0.4.6}/tests/refactor_golden/hex_bar.json +0 -0
  151. {draftwright-0.4.5 → draftwright-0.4.6}/tests/refactor_golden/holed_slot.json +0 -0
  152. {draftwright-0.4.5 → draftwright-0.4.6}/tests/refactor_golden/pocketed.json +0 -0
  153. {draftwright-0.4.5 → draftwright-0.4.6}/tests/refactor_golden/prismatic_ladder.json +0 -0
  154. {draftwright-0.4.5 → draftwright-0.4.6}/tests/refactor_golden/scattered_plate.json +0 -0
  155. {draftwright-0.4.5 → draftwright-0.4.6}/tests/refactor_golden/side_drilled.json +0 -0
  156. {draftwright-0.4.5 → draftwright-0.4.6}/tests/refactor_golden/turned_stepped.json +0 -0
  157. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_add_dimension.py +0 -0
  158. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_agents_guide.py +0 -0
  159. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_api_docs.py +0 -0
  160. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_architecture_docs.py +0 -0
  161. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_audit_differential.py +0 -0
  162. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_balloon_ring_standoff.py +0 -0
  163. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_carve_free_position_callers.py +0 -0
  164. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_compiled_plan_boundary.py +0 -0
  165. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_counting_calls.py +0 -0
  166. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_dense_sheet_canary.py +0 -0
  167. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_deprecation_dates.py +0 -0
  168. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_dimension_role_vocabulary.py +0 -0
  169. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_drawing_encapsulation.py +0 -0
  170. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_e2e_slice.py +0 -0
  171. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_e2e_standards.py +0 -0
  172. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_export_dxf_curves.py +0 -0
  173. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_export_dxf_zoom.py +0 -0
  174. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_fits.py +0 -0
  175. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_gdt_placement.py +0 -0
  176. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_issue_1000_envelope_reuse.py +0 -0
  177. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_issue_1086_declared_gears.py +0 -0
  178. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_issue_1142_hole_leader_labels.py +0 -0
  179. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_issue_1144_transactional_hole_table.py +0 -0
  180. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_issue_1147_legibility_pair_aggregation.py +0 -0
  181. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_issue_676_polygonal_boss_declare.py +0 -0
  182. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_issue_915_dense_case.py +0 -0
  183. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_issue_916_pocket_leader.py +0 -0
  184. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_issue_955_height_contingency.py +0 -0
  185. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_label_provenance.py +0 -0
  186. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_layout.py +0 -0
  187. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_layout_cleanliness.py +0 -0
  188. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_layout_hypothesis.py +0 -0
  189. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_layout_property.py +0 -0
  190. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_leader_footprint.py +0 -0
  191. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_lint_box_cache.py +0 -0
  192. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_lint_reconciliation.py +0 -0
  193. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_lint_structural.py +0 -0
  194. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_linting.py +0 -0
  195. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_location_vocabulary.py +0 -0
  196. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_note_verb.py +0 -0
  197. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_object_aspects.py +0 -0
  198. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_occupancy_boxes.py +0 -0
  199. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_parameter_id.py +0 -0
  200. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_pitch_dim_footprint.py +0 -0
  201. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_pmi.py +0 -0
  202. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_pmi_datum_lowering.py +0 -0
  203. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_pmi_gtol_lowering.py +0 -0
  204. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_pmi_part21.py +0 -0
  205. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_pocket_pattern.py +0 -0
  206. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_principal_profile_classifier.py +0 -0
  207. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_private_test_attr_reads.py +0 -0
  208. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_refactor_golden.py +0 -0
  209. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_registry.py +0 -0
  210. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_render_seam.py +0 -0
  211. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_repack_geometry_seam.py +0 -0
  212. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_score.py +0 -0
  213. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_script_detail_parity.py +0 -0
  214. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_shared_box_memo.py +0 -0
  215. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_sheet_emit.py +0 -0
  216. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_sheet_gdt.py +0 -0
  217. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_sheet_identity_invariant.py +0 -0
  218. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_sheet_notes.py +0 -0
  219. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_sheet_of.py +0 -0
  220. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_sheet_section.py +0 -0
  221. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_sheet_tables.py +0 -0
  222. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_slot_pattern.py +0 -0
  223. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_soft_deprecation.py +0 -0
  224. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_solve_trace.py +0 -0
  225. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_strip_layout.py +0 -0
  226. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_suppression_ledger.py +0 -0
  227. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_suppression_marks.py +0 -0
  228. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_tolerances.py +0 -0
  229. {draftwright-0.4.5 → draftwright-0.4.6}/tests/test_witness_label_reconciliation.py +0 -0
@@ -1,6 +1,30 @@
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.
4
28
 
5
29
  ## v0.4.5 — 2026-08-15
6
30
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: draftwright
3
- Version: 0.4.5
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.5"
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
 
@@ -1881,6 +1881,7 @@ def render_flats(dwg, plan, a, *, ctx, only=None) -> int:
1881
1881
  collapse.setdefault(
1882
1882
  (
1883
1883
  g.facts.axis,
1884
+ g.facts.presentation_axis,
1884
1885
  g.facts.axis_direction,
1885
1886
  g.facts.axis_line,
1886
1887
  g.facts.stock_span,
@@ -1889,7 +1890,7 @@ def render_flats(dwg, plan, a, *, ctx, only=None) -> int:
1889
1890
  [],
1890
1891
  ).append((g, pd))
1891
1892
  jobs = []
1892
- for gi, ((axis, _direction, _line, _span, _across), members) in enumerate(
1893
+ for gi, ((_axis, presentation_axis, _direction, _line, _span, _across), members) in enumerate(
1893
1894
  sorted(collapse.items())
1894
1895
  ):
1895
1896
  if only is not None:
@@ -1920,7 +1921,7 @@ def render_flats(dwg, plan, a, *, ctx, only=None) -> int:
1920
1921
  )
1921
1922
  jobs.append(
1922
1923
  (
1923
- f"m_flat_{axis}{gi}",
1924
+ f"m_flat_{presentation_axis}{gi}",
1924
1925
  view,
1925
1926
  vb,
1926
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,7 +42,6 @@ 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,
@@ -1885,15 +1884,25 @@ def _needs_section(feat: HoleFeature | PatternFeature):
1885
1884
  return bore.cbore is not None or bore.spotface is not None or not bore.through
1886
1885
 
1887
1886
 
1888
- def _leader_hits(leader, tip, elbow, side, obstacles):
1887
+ def _leader_hits(leader, tip, elbow, side, obstacles, draft):
1889
1888
  """True when *leader*'s rendered footprint truly overlaps any *obstacles* box — split into
1890
- the diagonal tip→elbow SHAFT (checked precisely via `_segment_crosses_box`, since its AABB
1891
- over-claims the empty triangle it doesn't occupy — #305) and the elbow→label shelf+text
1892
- (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)."""
1893
1893
  full_box = _geom_box(leader)
1894
1894
  if full_box is None or not _box_hits(full_box, obstacles):
1895
1895
  return False # fast reject: nowhere near any obstacle
1896
- 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
+ ):
1897
1906
  return True
1898
1907
  label_box = (
1899
1908
  (elbow[0], full_box[1], full_box[2], full_box[3])
@@ -2433,7 +2442,7 @@ def _place_queue(
2433
2442
  continue
2434
2443
  y = final_y[tid]
2435
2444
  leader, tip, elbow = _build_leader_at(s, edge, side, y, to_page, elbow_dx, draft, a.SCALE)
2436
- if _leader_hits(leader, tip, elbow, side, occupied):
2445
+ if _leader_hits(leader, tip, elbow, side, occupied, draft):
2437
2446
  crossing.append((s, y, leader))
2438
2447
  else:
2439
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
 
@@ -24,20 +24,7 @@ from collections.abc import Iterable
24
24
  from math import atan2, pi, tau
25
25
  from typing import Literal
26
26
 
27
- from build123d import GeomType
28
- from build123d_drafting.helpers import CenterMark, Dimension, TitleBlock
29
-
30
- from draftwright._core import (
31
- _END_ON,
32
- HoleRef,
33
- _annotation_diameter_sources,
34
- _axis_letter,
35
- _fmt,
36
- _xyz,
37
- )
38
- from draftwright.linting.issues import LintIssue
39
- from draftwright.linting.profiled_bore_coverage import profiled_bore_key
40
- from draftwright.recognition import (
27
+ from b123d_recognisers import (
41
28
  RecognitionResult,
42
29
  TurnedProfile,
43
30
  analyse_cylinders,
@@ -51,11 +38,24 @@ from draftwright.recognition import (
51
38
  recognise_rectangular_pads,
52
39
  recognise_turned_steps,
53
40
  )
54
- from draftwright.recognition.profiled_bores import (
41
+ from b123d_recognisers.profiled_bores import (
55
42
  double_d_bores_from_openings,
56
43
  double_d_profile,
57
44
  principal_boundary_plane,
58
45
  )
46
+ from build123d import GeomType
47
+ from build123d_drafting.helpers import CenterMark, Dimension, TitleBlock
48
+
49
+ from draftwright._core import (
50
+ _END_ON,
51
+ HoleRef,
52
+ _annotation_diameter_sources,
53
+ _axis_letter,
54
+ _fmt,
55
+ _xyz,
56
+ )
57
+ from draftwright.linting.issues import LintIssue
58
+ from draftwright.linting.profiled_bore_coverage import profiled_bore_key
59
59
 
60
60
  _UNSET = object() # sentinel: distinguishes "not supplied" from a valid prof=None
61
61
 
@@ -11,9 +11,10 @@ from collections import defaultdict
11
11
  from dataclasses import dataclass
12
12
  from typing import Literal
13
13
 
14
+ from b123d_recognisers import RecognitionResult
15
+
14
16
  from draftwright._geometry import _canonical_axis_direction
15
17
  from draftwright.linting.issues import LintIssue
16
- from draftwright.recognition import RecognitionResult
17
18
 
18
19
  FlatRequirementState = Literal["placed", "suppressed", "dropped", "missing", "unverifiable"]
19
20
 
@@ -12,9 +12,10 @@ from dataclasses import dataclass
12
12
  from math import hypot
13
13
  from typing import Literal
14
14
 
15
+ from b123d_recognisers import HoleSpec, RecognitionResult, countersink_matches_hole
16
+
15
17
  from draftwright._geometry import _is_principal_axis
16
18
  from draftwright.linting.issues import LintIssue
17
- from draftwright.recognition import HoleSpec, RecognitionResult, countersink_matches_hole
18
19
 
19
20
  HoleRequirementState = Literal["placed", "suppressed", "dropped", "missing", "unverifiable"]
20
21
  HoleSourceKind = Literal["hole", "hole_pattern"]
@@ -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
9
+
8
10
  from draftwright.linting.issues import LintIssue
9
- from draftwright.recognition import RecognitionResult
10
11
 
11
12
  PolygonalStockState = Literal["placed", "suppressed", "dropped", "missing", "unverifiable"]
12
13
 
@@ -11,9 +11,10 @@ from collections import Counter
11
11
  from math import hypot, isfinite
12
12
  from typing import Literal
13
13
 
14
+ from b123d_recognisers import RecognitionResult
15
+
14
16
  from draftwright._geometry import _fmt
15
17
  from draftwright.linting.issues import LintIssue
16
- from draftwright.recognition import RecognitionResult
17
18
 
18
19
 
19
20
  def _axis_letter(value) -> str:
@@ -12,8 +12,9 @@ from collections import defaultdict
12
12
  from dataclasses import dataclass
13
13
  from typing import Literal
14
14
 
15
+ from b123d_recognisers import RecognitionResult
16
+
15
17
  from draftwright.linting.issues import LintIssue
16
- from draftwright.recognition import RecognitionResult
17
18
 
18
19
  SlotRequirementState = Literal["placed", "suppressed", "dropped", "missing", "unverifiable"]
19
20
  SlotSourceKind = Literal["slot", "slot_pattern"]
@@ -272,7 +272,14 @@ _FACTS: dict[str, tuple[str, ...]] = {
272
272
  # These are STRUCTURE, not measurements: together they say which piece of stock a flat
273
273
  # belongs to, so the renderer can tell one double-D's two faces from independent aligned
274
274
  # or slanted regions (#1013/#1036). The drawing prints none of them.
275
- "flat": ("frame", "axis", "axis_line", "stock_span", "axis_direction"),
275
+ "flat": (
276
+ "frame",
277
+ "axis",
278
+ "presentation_axis",
279
+ "axis_line",
280
+ "stock_span",
281
+ "axis_direction",
282
+ ),
276
283
  "groove": ("frame", "axis"),
277
284
  "plate": ("frame", "axis"),
278
285
  # Raw AP242 PMI is the documented non-generated exception: its source-authored label
@@ -290,7 +290,7 @@ def double_d_bore(
290
290
  with exactly that boundary; arbitrary line/arc profiles and blind profiles fail closed.
291
291
  """
292
292
  if obj is not None:
293
- from draftwright.recognition.profiled_bores import read_double_d_tool
293
+ from b123d_recognisers.profiled_bores import read_double_d_tool
294
294
 
295
295
  r_axis, r_major, r_af, r_at, r_depth, r_direction = read_double_d_tool(obj)
296
296
  axis = r_axis if axis is None else axis
@@ -329,12 +329,11 @@ def read_countersink(cone) -> tuple[float, float]:
329
329
  """``(major_diameter, included_angle°)`` of a countersink **cone** tool — the larger rim ⌀
330
330
  and the full cone angle, read off its conical **face** (not a removed edge, #576 lesson).
331
331
  The cone geometry is the recogniser's own
332
- :func:`~draftwright.recognition.countersinks.cone_rims` (#704), so a declared
332
+ :func:`~b123d_recognisers.countersinks.cone_rims` (#704), so a declared
333
333
  countersink reads identically to a detected one by construction."""
334
+ from b123d_recognisers import cone_rims
334
335
  from build123d import GeomType
335
336
 
336
- from draftwright.recognition import cone_rims
337
-
338
337
  faces = cone.faces().filter_by(GeomType.CONE)
339
338
  if not faces:
340
339
  raise ValueError("countersink(cone=...) needs a conical tool (a build123d Cone)")
@@ -506,10 +505,10 @@ def _read_chamfer_face(face) -> tuple[str, float, float, Point]:
506
505
  """Read a chamfer off its **oblique planar bevel face**: the axis (the edge the chamfer
507
506
  runs along), the two legs (the face's in-plane extents), and a point **on the bevel** (the
508
507
  face centre — not the removed sharp corner). The classification + leg geometry is the
509
- recogniser's own :func:`~draftwright.recognition.chamfers.classify_bevel` (#704), so a
508
+ recogniser's own :func:`~b123d_recognisers.chamfers.classify_bevel` (#704), so a
510
509
  declared chamfer reads identically to a detected one by construction; only the
511
510
  user-facing error messages live here."""
512
- from draftwright.recognition import BevelReject, classify_bevel
511
+ from b123d_recognisers import BevelReject, classify_bevel
513
512
 
514
513
  try:
515
514
  edge_i, _nv, _span, hi, lo = classify_bevel(face)
@@ -597,7 +596,7 @@ def _read_fillet_face(face) -> tuple[str, float, Point]:
597
596
  # Anchor on the curved radius surface itself — the recogniser's own fillet_anchor
598
597
  # (#622 lesson: never the bbox centre), so a declared fillet's leader tip is
599
598
  # identical to the detected one's by construction (#704).
600
- from draftwright.recognition import fillet_anchor
599
+ from b123d_recognisers import fillet_anchor
601
600
 
602
601
  p = fillet_anchor(s)
603
602
  return (
@@ -722,7 +721,7 @@ def _read_groove_face(face) -> tuple[str, float, float, Point]:
722
721
  span = ((bb.min.X, bb.max.X), (bb.min.Y, bb.max.Y), (bb.min.Z, bb.max.Z))[axis_i]
723
722
  # The leader tip is the recogniser's own floor_face_anchor (#704), so a declared
724
723
  # groove anchors identically to a detected one by construction.
725
- from draftwright.recognition import floor_face_anchor
724
+ from b123d_recognisers import floor_face_anchor
726
725
 
727
726
  c = floor_face_anchor(face)
728
727
  return (
@@ -874,7 +873,7 @@ def _read_step_levels(
874
873
  mirroring ``model/detect.py``), the ``datum`` (bbox min corner the positions measure from)
875
874
  and ``at`` (the frame anchor — bbox centre X/Y at ``base``, matching ``detect.py`` so an
876
875
  object round-trips to the same IR)."""
877
- from draftwright.recognition import (
876
+ from b123d_recognisers import (
878
877
  project_step_shoulders,
879
878
  recognise_risers,
880
879
  step_level_records,