draftwright 0.4.2__tar.gz → 0.4.3__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 (209) hide show
  1. {draftwright-0.4.2 → draftwright-0.4.3}/CHANGELOG.md +31 -0
  2. {draftwright-0.4.2 → draftwright-0.4.3}/PKG-INFO +1 -1
  3. {draftwright-0.4.2 → draftwright-0.4.3}/pyproject.toml +1 -1
  4. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/analysis.py +2 -0
  5. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/annotations/from_model.py +57 -16
  6. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/annotations/orchestrator.py +3 -0
  7. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/compose.py +9 -6
  8. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/drawing.py +11 -0
  9. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/linting/__init__.py +2 -0
  10. draftwright-0.4.3/src/draftwright/linting/polygonal_stock_coverage.py +114 -0
  11. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/model/__init__.py +4 -0
  12. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/model/compiled.py +7 -0
  13. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/model/declare.py +29 -0
  14. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/model/detect.py +30 -1
  15. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/model/ir.py +48 -0
  16. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/recognition/__init__.py +8 -1
  17. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/recognition/polygonal_bosses.py +80 -10
  18. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/recognition/result.py +7 -1
  19. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/recognition/slots.py +48 -2
  20. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/sheet.py +6 -0
  21. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/sheet_emit.py +16 -0
  22. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_add_dimension.py +9 -0
  23. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_compiled_plan_boundary.py +1 -0
  24. draftwright-0.4.3/tests/test_issue_1073_cross_body_patterns.py +95 -0
  25. draftwright-0.4.3/tests/test_issue_1082_polygonal_stock.py +232 -0
  26. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_make_drawing.py +31 -4
  27. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_parameter_id.py +27 -0
  28. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_recognition_manifest.py +7 -0
  29. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_recognition_result.py +2 -0
  30. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_sheet_emit.py +10 -0
  31. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_sheet_identity_invariant.py +1 -0
  32. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_workflows.py +8 -0
  33. {draftwright-0.4.2 → draftwright-0.4.3}/.gitignore +0 -0
  34. {draftwright-0.4.2 → draftwright-0.4.3}/LICENSE +0 -0
  35. {draftwright-0.4.2 → draftwright-0.4.3}/README.md +0 -0
  36. {draftwright-0.4.2 → draftwright-0.4.3}/docs/adr/README.md +0 -0
  37. {draftwright-0.4.2 → draftwright-0.4.3}/skills/SKILL.md +0 -0
  38. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/__init__.py +0 -0
  39. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/_core.py +0 -0
  40. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/_geometry.py +0 -0
  41. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/_pmi_part21.py +0 -0
  42. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/_warnings.py +0 -0
  43. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/annotate.py +0 -0
  44. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/annotations/__init__.py +0 -0
  45. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/annotations/_common.py +0 -0
  46. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/annotations/balloons.py +0 -0
  47. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/annotations/holes.py +0 -0
  48. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/annotations/sections.py +0 -0
  49. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/audit.py +0 -0
  50. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/builder.py +0 -0
  51. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/cli.py +0 -0
  52. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/export.py +0 -0
  53. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/fits.py +0 -0
  54. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
  55. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
  56. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
  57. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
  58. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/fonts/__init__.py +0 -0
  59. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/intents.py +0 -0
  60. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/layout.py +0 -0
  61. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/linting/channel_coverage.py +0 -0
  62. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/linting/coverage.py +0 -0
  63. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/linting/flat_coverage.py +0 -0
  64. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/linting/issues.py +0 -0
  65. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/linting/pmi_coverage.py +0 -0
  66. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/linting/profiled_bore_coverage.py +0 -0
  67. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/linting/slot_coverage.py +0 -0
  68. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/linting/structural.py +0 -0
  69. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/linting/suggest.py +0 -0
  70. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/make_drawing.py +0 -0
  71. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/model/callout.py +0 -0
  72. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/model/planner.py +0 -0
  73. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/pmi.py +0 -0
  74. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/projection.py +0 -0
  75. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/py.typed +0 -0
  76. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/recognition/_features.py +0 -0
  77. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/recognition/_record.py +0 -0
  78. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/recognition/chamfers.py +0 -0
  79. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/recognition/countersinks.py +0 -0
  80. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/recognition/fillets.py +0 -0
  81. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/recognition/flats.py +0 -0
  82. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/recognition/grooves.py +0 -0
  83. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/recognition/levels.py +0 -0
  84. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/recognition/pads.py +0 -0
  85. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/recognition/plates.py +0 -0
  86. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/recognition/profiled_bores.py +0 -0
  87. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/recognition/turned.py +0 -0
  88. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/registry.py +0 -0
  89. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/repair.py +0 -0
  90. {draftwright-0.4.2 → draftwright-0.4.3}/src/draftwright/score.py +0 -0
  91. {draftwright-0.4.2 → draftwright-0.4.3}/tests/_kernel.py +0 -0
  92. {draftwright-0.4.2 → draftwright-0.4.3}/tests/_layout_sig.py +0 -0
  93. {draftwright-0.4.2 → draftwright-0.4.3}/tests/conftest.py +0 -0
  94. {draftwright-0.4.2 → draftwright-0.4.3}/tests/fixtures/grm03_thumbwheel_drive_screw.step +0 -0
  95. {draftwright-0.4.2 → draftwright-0.4.3}/tests/fixtures/if_step_flat_across_cylinder.step +0 -0
  96. {draftwright-0.4.2 → draftwright-0.4.3}/tests/fixtures/issue_1058_wheel_rh.step +0 -0
  97. {draftwright-0.4.2 → draftwright-0.4.3}/tests/fixtures/issue_909_basic_part_design_017_body.step +0 -0
  98. {draftwright-0.4.2 → draftwright-0.4.3}/tests/fixtures/issue_915_case_study_2.step +0 -0
  99. {draftwright-0.4.2 → draftwright-0.4.3}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
  100. {draftwright-0.4.2 → draftwright-0.4.3}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
  101. {draftwright-0.4.2 → draftwright-0.4.3}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
  102. {draftwright-0.4.2 → draftwright-0.4.3}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
  103. {draftwright-0.4.2 → draftwright-0.4.3}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
  104. {draftwright-0.4.2 → draftwright-0.4.3}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
  105. {draftwright-0.4.2 → draftwright-0.4.3}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
  106. {draftwright-0.4.2 → draftwright-0.4.3}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
  107. {draftwright-0.4.2 → draftwright-0.4.3}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
  108. {draftwright-0.4.2 → draftwright-0.4.3}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
  109. {draftwright-0.4.2 → draftwright-0.4.3}/tests/fixtures/tuner_jig_blind_obround_pockets.step +0 -0
  110. {draftwright-0.4.2 → draftwright-0.4.3}/tests/refactor_golden/bracket_section.json +0 -0
  111. {draftwright-0.4.2 → draftwright-0.4.3}/tests/refactor_golden/centered_rebate.json +0 -0
  112. {draftwright-0.4.2 → draftwright-0.4.3}/tests/refactor_golden/chamfered.json +0 -0
  113. {draftwright-0.4.2 → draftwright-0.4.3}/tests/refactor_golden/filleted.json +0 -0
  114. {draftwright-0.4.2 → draftwright-0.4.3}/tests/refactor_golden/flange_dense.json +0 -0
  115. {draftwright-0.4.2 → draftwright-0.4.3}/tests/refactor_golden/grid_plate.json +0 -0
  116. {draftwright-0.4.2 → draftwright-0.4.3}/tests/refactor_golden/grooved_shaft.json +0 -0
  117. {draftwright-0.4.2 → draftwright-0.4.3}/tests/refactor_golden/hex_bar.json +0 -0
  118. {draftwright-0.4.2 → draftwright-0.4.3}/tests/refactor_golden/holed_slot.json +0 -0
  119. {draftwright-0.4.2 → draftwright-0.4.3}/tests/refactor_golden/pocketed.json +0 -0
  120. {draftwright-0.4.2 → draftwright-0.4.3}/tests/refactor_golden/prismatic_ladder.json +0 -0
  121. {draftwright-0.4.2 → draftwright-0.4.3}/tests/refactor_golden/scattered_plate.json +0 -0
  122. {draftwright-0.4.2 → draftwright-0.4.3}/tests/refactor_golden/side_drilled.json +0 -0
  123. {draftwright-0.4.2 → draftwright-0.4.3}/tests/refactor_golden/turned_stepped.json +0 -0
  124. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_agents_guide.py +0 -0
  125. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_api_docs.py +0 -0
  126. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_architecture_docs.py +0 -0
  127. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_audit_differential.py +0 -0
  128. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_balloon_ring_standoff.py +0 -0
  129. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_carve_free_position_callers.py +0 -0
  130. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_counting_calls.py +0 -0
  131. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_declare.py +0 -0
  132. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_declared_recognition_gate.py +0 -0
  133. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_dense_sheet_canary.py +0 -0
  134. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_deprecation_dates.py +0 -0
  135. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_detect_once.py +0 -0
  136. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_detect_registry.py +0 -0
  137. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_dimension_role_vocabulary.py +0 -0
  138. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_drawing_encapsulation.py +0 -0
  139. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_e2e_slice.py +0 -0
  140. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_e2e_standards.py +0 -0
  141. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_export_dxf_curves.py +0 -0
  142. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_export_dxf_zoom.py +0 -0
  143. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_fillets_adjacency.py +0 -0
  144. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_fits.py +0 -0
  145. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_flat_completeness.py +0 -0
  146. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_flat_stock_identity.py +0 -0
  147. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_flat_stock_opposition.py +0 -0
  148. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_gdt_placement.py +0 -0
  149. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_grid_lattice_convention.py +0 -0
  150. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_import_boundaries.py +0 -0
  151. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_issue_1000_envelope_reuse.py +0 -0
  152. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_issue_1058_wheel_profile.py +0 -0
  153. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_issue_676_polygonal_boss.py +0 -0
  154. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_issue_676_polygonal_boss_declare.py +0 -0
  155. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_issue_885_prismatic_coverage.py +0 -0
  156. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_issue_909_sloped_profile.py +0 -0
  157. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_issue_915_dense_case.py +0 -0
  158. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_issue_916_pocket_leader.py +0 -0
  159. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_issue_917_open_channel.py +0 -0
  160. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_issue_955_height_contingency.py +0 -0
  161. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_issue_958_cross_solid_recognition.py +0 -0
  162. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_label_provenance.py +0 -0
  163. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_layout.py +0 -0
  164. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_layout_cleanliness.py +0 -0
  165. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_layout_hypothesis.py +0 -0
  166. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_layout_property.py +0 -0
  167. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_lint_box_cache.py +0 -0
  168. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_lint_reconciliation.py +0 -0
  169. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_lint_structural.py +0 -0
  170. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_linting.py +0 -0
  171. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_location_vocabulary.py +0 -0
  172. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_note_verb.py +0 -0
  173. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_object_aspects.py +0 -0
  174. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_occupancy_boxes.py +0 -0
  175. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_part_model.py +0 -0
  176. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_pitch_dim_footprint.py +0 -0
  177. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_pmi.py +0 -0
  178. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_pmi_datum_lowering.py +0 -0
  179. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_pmi_gtol_lowering.py +0 -0
  180. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_pmi_part21.py +0 -0
  181. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_pocket_pattern.py +0 -0
  182. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_pocket_pattern_recognition.py +0 -0
  183. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_principal_profile_classifier.py +0 -0
  184. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_private_test_attr_reads.py +0 -0
  185. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_private_test_imports.py +0 -0
  186. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_recogniser_contract.py +0 -0
  187. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_recognition.py +0 -0
  188. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_refactor_golden.py +0 -0
  189. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_registry.py +0 -0
  190. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_render_seam.py +0 -0
  191. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_score.py +0 -0
  192. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_script_detail_parity.py +0 -0
  193. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_sheet_gdt.py +0 -0
  194. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_sheet_notes.py +0 -0
  195. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_sheet_of.py +0 -0
  196. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_sheet_section.py +0 -0
  197. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_sheet_tables.py +0 -0
  198. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_slanted_blind_step.py +0 -0
  199. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_slot_completeness.py +0 -0
  200. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_slot_pattern.py +0 -0
  201. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_slot_pattern_recognition.py +0 -0
  202. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_soft_deprecation.py +0 -0
  203. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_solve_trace.py +0 -0
  204. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_strip_layout.py +0 -0
  205. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_suppression_ledger.py +0 -0
  206. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_suppression_marks.py +0 -0
  207. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_tolerances.py +0 -0
  208. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_turned_steps.py +0 -0
  209. {draftwright-0.4.2 → draftwright-0.4.3}/tests/test_witness_label_reconciliation.py +0 -0
@@ -2,6 +2,37 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## v0.4.3 — 2026-08-09
6
+
7
+ **A trust and validation-throughput patch release.** Whole-part hexagonal stock now receives its
8
+ manufacturing definition, assembly recognition no longer combines evidence across bodies, and
9
+ performance validation is both more stable and faster to run.
10
+
11
+ ### Added
12
+
13
+ - **Whole-part regular hexagonal stock is recognised independently of an attached boss and
14
+ dimensioned as `HEX … A/F` plus its axial length** (#1082). In-plane rotation preserves the
15
+ geometric across-flats size instead of substituting an orientation-dependent bounding-box
16
+ width. Automatic and declared paths share the same compiler and placement machinery, while
17
+ completeness lint distinguishes placed, authored-suppressed, dropped, missing, and ambiguous
18
+ outcomes.
19
+
20
+ ### Fixed
21
+
22
+ - **Slot and pocket patterns cannot group members from separate assembly bodies** (#1073).
23
+ Recognition carries solid ownership through grouping and fails closed when ownership is
24
+ unavailable, preventing plausible-looking phantom patterns across disconnected parts.
25
+
26
+ - **The annotation-obstacle performance guard uses deterministic work-count evidence instead of
27
+ a wall-clock threshold** (#1071). The regression test remains load-bearing under mutation but
28
+ no longer flakes because of machine load or suite ordering.
29
+
30
+ ### Changed
31
+
32
+ - **The post-merge slow tier distributes individual tests across workers** (#1108), using
33
+ `pytest-xdist`'s load scheduling after local benchmark evidence showed a material speed-up
34
+ without weakening fixture coverage. Hosted slow validation remains the single post-merge gate.
35
+
5
36
  ## v0.4.2 — 2026-08-09
6
37
 
7
38
  **The trustworthy manufacturing drawings release.** AP242 PMI is now accounted for from its
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: draftwright
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: Automated technical-drawing generation for build123d
5
5
  Project-URL: Homepage, https://github.com/pzfreo/draftwright
6
6
  Project-URL: Repository, https://github.com/pzfreo/draftwright
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "draftwright"
7
- version = "0.4.2"
7
+ version = "0.4.3"
8
8
  description = "Automated technical-drawing generation for build123d"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -627,6 +627,7 @@ def _analyse(
627
627
  patterns = list(recognition.hole_patterns) if recognition else []
628
628
  bosses = list(recognition.bosses) if recognition else []
629
629
  polygonal_bosses = list(recognition.polygonal_bosses) if recognition else []
630
+ polygonal_stock = list(recognition.polygonal_stock) if recognition else []
630
631
  slots = list(recognition.slots) if recognition else []
631
632
  pockets = list(recognition.pockets) if recognition else []
632
633
  pocket_patterns = list(recognition.pocket_patterns) if recognition else []
@@ -655,6 +656,7 @@ def _analyse(
655
656
  patterns=patterns,
656
657
  bosses=bosses,
657
658
  polygonal_bosses=polygonal_bosses,
659
+ polygonal_stock=polygonal_stock,
658
660
  channels=list(recognition.channels) if recognition else None,
659
661
  slots=slots,
660
662
  # Injected from the aggregate since #1026 — `build_part_model` detected these
@@ -2195,13 +2195,14 @@ def _polygonal_boss_candidates(dwg, view, vb, boss, reach, *, provenance):
2195
2195
  yield (tip, elbow, provenance)
2196
2196
 
2197
2197
 
2198
- def render_polygonal_bosses(dwg, plan, a, *, ctx) -> int:
2199
- """Across-flats leaders for bounded regular polygonal bosses (#676)."""
2198
+ def _render_polygonal_prisms(
2199
+ dwg, plan, a, *, ctx, kind: str, noun: str, name_prefix: str, drop_code: str
2200
+ ) -> int:
2200
2201
  draft = dwg.draft
2201
2202
  reach = _leader_callout_reach(draft)
2202
2203
  jobs = []
2203
2204
  groups = sorted(
2204
- plan.of_kind("polygonal_boss"),
2205
+ plan.of_kind(kind),
2205
2206
  key=lambda group: (group.facts.frame.axis, group.facts.frame.origin),
2206
2207
  )
2207
2208
  for index, group in enumerate(groups):
@@ -2226,7 +2227,7 @@ def render_polygonal_bosses(dwg, plan, a, *, ctx) -> int:
2226
2227
  label = f"{prefix} {dimension.value_text}{_tol_suffix(dimension.tolerance, draft)} A/F"
2227
2228
  jobs.append(
2228
2229
  (
2229
- f"m_polygonal_boss_{boss.frame.axis}{index}",
2230
+ f"{name_prefix}_{boss.frame.axis}{index}",
2230
2231
  view,
2231
2232
  bounds,
2232
2233
  label,
@@ -2245,15 +2246,43 @@ def render_polygonal_bosses(dwg, plan, a, *, ctx) -> int:
2245
2246
  dwg,
2246
2247
  a,
2247
2248
  jobs,
2249
+ noun=noun,
2250
+ drop_code=drop_code,
2251
+ ctx=ctx,
2252
+ geom_clear=True,
2253
+ )
2254
+
2255
+
2256
+ def render_polygonal_bosses(dwg, plan, a, *, ctx) -> int:
2257
+ """Across-flats leaders for bounded regular polygonal bosses (#676)."""
2258
+ return _render_polygonal_prisms(
2259
+ dwg,
2260
+ plan,
2261
+ a,
2262
+ ctx=ctx,
2263
+ kind="polygonal_boss",
2248
2264
  noun="polygonal boss",
2265
+ name_prefix="m_polygonal_boss",
2249
2266
  drop_code="polygonal_boss_dropped",
2267
+ )
2268
+
2269
+
2270
+ def render_polygonal_stock(dwg, plan, a, *, ctx) -> int:
2271
+ """Across-flats leaders for whole regular polygonal stock (#1082)."""
2272
+ return _render_polygonal_prisms(
2273
+ dwg,
2274
+ plan,
2275
+ a,
2250
2276
  ctx=ctx,
2251
- geom_clear=True,
2277
+ kind="polygonal_stock",
2278
+ noun="polygonal stock",
2279
+ name_prefix="m_polygonal_stock",
2280
+ drop_code="polygonal_stock_dropped",
2252
2281
  )
2253
2282
 
2254
2283
 
2255
2284
  def render_boss_heights(dwg, plan, a, *, ctx) -> int:
2256
- """Queue the axial height of each prismatic boss in a profile-view corridor (#632)."""
2285
+ """Queue prismatic boss heights and polygonal-stock lengths in a profile corridor."""
2257
2286
  if a.is_rotational or a.prof is not None:
2258
2287
  return 0
2259
2288
  tier = dwg.draft.font_size + 2 * dwg.draft.pad_around_text
@@ -2265,15 +2294,17 @@ def render_boss_heights(dwg, plan, a, *, ctx) -> int:
2265
2294
  n = 0
2266
2295
  for bi, g in enumerate(
2267
2296
  sorted(
2268
- [*plan.of_kind("boss"), *plan.of_kind("polygonal_boss")],
2297
+ [
2298
+ *plan.of_kind("boss"),
2299
+ *plan.of_kind("polygonal_boss"),
2300
+ *plan.of_kind("polygonal_stock"),
2301
+ ],
2269
2302
  key=lambda g: (g.facts.frame.axis, g.facts.frame.origin),
2270
2303
  )
2271
2304
  ):
2272
2305
  b = g.facts
2273
- pd = next(
2274
- (d for d in g.dims if (d.role, d.kind) == ("boss_height", "length")),
2275
- None,
2276
- )
2306
+ role = "stock_length" if g.ref.kind == "polygonal_stock" else "boss_height"
2307
+ pd = next((d for d in g.dims if (d.role, d.kind) == (role, "length")), None)
2277
2308
  if pd is None or pd.span is None:
2278
2309
  continue
2279
2310
  spec = specs.get(b.frame.axis)
@@ -2284,7 +2315,8 @@ def render_boss_heights(dwg, plan, a, *, ctx) -> int:
2284
2315
  p2 = dwg.at(view, *pd.span[1])
2285
2316
  edge = max(p1[0], p2[0]) if side == "right" else max(p1[1], p2[1])
2286
2317
  label = pd.value_text + _tol_suffix(pd.tolerance, dwg.draft)
2287
- name = f"m_bossheight_{b.frame.axis}{bi}"
2318
+ prefix = "m_stocklength" if g.ref.kind == "polygonal_stock" else "m_bossheight"
2319
+ name = f"{prefix}_{b.frame.axis}{bi}"
2288
2320
 
2289
2321
  def build(pos, p1=p1, p2=p2, side=side, edge=edge, label=label):
2290
2322
  return _dim(p1, p2, side, abs(pos - edge), dwg.draft, label=label)
@@ -2292,6 +2324,15 @@ def render_boss_heights(dwg, plan, a, *, ctx) -> int:
2292
2324
  def footprint(pos, p1=p1, p2=p2, side=side, edge=edge, label=label):
2293
2325
  return dim_footprint(p1, p2, side, abs(pos - edge), dwg.draft, label)
2294
2326
 
2327
+ def dropped(_name, *, stock=g.ref.kind == "polygonal_stock", measurement=pd.id):
2328
+ if stock:
2329
+ ctx.record_issue(
2330
+ "warning",
2331
+ "polygonal_stock_length_dropped",
2332
+ "polygonal stock axial length was not placed (profile strip full)",
2333
+ measurement=measurement,
2334
+ )
2335
+
2295
2336
  register_corridor(
2296
2337
  ctx,
2297
2338
  (view, side),
@@ -2304,10 +2345,10 @@ def render_boss_heights(dwg, plan, a, *, ctx) -> int:
2304
2345
  build=build,
2305
2346
  order=(_SIZE_SUBCHAIN, bi, name),
2306
2347
  on_place=lambda _nm: None,
2307
- # Live feature reconciliation below Drawing.lint() is authoritative:
2308
- # an absent height becomes one boss_height_missing issue. Recording a
2309
- # second build-time drop here would double-count the same omission.
2310
- on_drop=lambda _nm: None,
2348
+ # Boss heights retain their historical reconciliation-only outcome. Stock
2349
+ # length additionally records the compiler measurement identity so its
2350
+ # completeness check can distinguish a placement drop from missing output.
2351
+ on_drop=dropped,
2311
2352
  force=True,
2312
2353
  feature=g.ref,
2313
2354
  measurement=pd.id,
@@ -55,6 +55,7 @@ from draftwright.annotations.from_model import (
55
55
  render_pmi,
56
56
  render_pockets,
57
57
  render_polygonal_bosses,
58
+ render_polygonal_stock,
58
59
  render_rotational,
59
60
  render_slots,
60
61
  render_step_lengths,
@@ -214,6 +215,7 @@ def build_model(a: Analysis):
214
215
  patterns=a.patterns,
215
216
  bosses=a.bosses,
216
217
  polygonal_bosses=a.recognition.polygonal_bosses,
218
+ polygonal_stock=a.recognition.polygonal_stock,
217
219
  channels=a.recognition.channels,
218
220
  slots=a.slots,
219
221
  # The engine's SECOND build_part_model call site; unthreaded, these three were
@@ -522,6 +524,7 @@ def _auto_annotate(dwg, a: Analysis, *, detail_view: bool = False):
522
524
 
523
525
  def _s_polygonal_bosses():
524
526
  render_polygonal_bosses(dwg, _compiled, a, ctx=ctx)
527
+ render_polygonal_stock(dwg, _compiled, a, ctx=ctx)
525
528
 
526
529
  def _s_diameters():
527
530
  # Turned-part dimensions via the IR (ADR 0008 convergence). The model is built
@@ -72,7 +72,7 @@ def _est_right_strip_depth(n_steps: int, n_extra: int = 0) -> float:
72
72
 
73
73
 
74
74
  def _n_right_strip_boss_heights(model) -> int:
75
- """How many boss-height dims will contend for the front view's right strip (#1022).
75
+ """How many axial-size dims need slots beyond the canonical overall-height slot (#1022).
76
76
 
77
77
  ``render_boss_heights`` puts a Z-axis boss's height there, sharing the strip with the step
78
78
  ladder — but the depth estimate counted only ladder steps, so the strip was sized for one
@@ -88,13 +88,16 @@ def _n_right_strip_boss_heights(model) -> int:
88
88
  """
89
89
  if any(getattr(f, "kind", None) == "step" for f in model.features):
90
90
  return 0
91
- return sum(
92
- 1
91
+ occupants = [
92
+ f
93
93
  for f in model.features
94
- if getattr(f, "kind", None) in ("boss", "polygonal_boss")
95
- and getattr(f, "height", None) is not None
94
+ if getattr(f, "kind", None) in ("boss", "polygonal_boss", "polygonal_stock")
95
+ and (getattr(f, "height", None) is not None or getattr(f, "length", None) is not None)
96
96
  and f.frame.axis == "z"
97
- )
97
+ ]
98
+ # Polygonal stock replaces (rather than supplements) `dim_height`, so its axial length
99
+ # uses the estimator's canonical first slot. Other boss heights remain extra occupants.
100
+ return max(0, len(occupants) - any(f.kind == "polygonal_stock" for f in occupants))
98
101
 
99
102
 
100
103
  def _est_pv_below_depth() -> float:
@@ -83,6 +83,7 @@ from draftwright.linting import (
83
83
  lint_pmi_ignored,
84
84
  lint_pmi_lowering,
85
85
  lint_pmi_rendering,
86
+ lint_polygonal_stock_coverage,
86
87
  lint_principal_profile_coverage,
87
88
  lint_prismatic_coverage,
88
89
  lint_profiled_bore_coverage,
@@ -129,6 +130,8 @@ _GEOMETRY_AWARE_CODES = frozenset(
129
130
  "channel_width_dropped",
130
131
  "flat_dropped",
131
132
  "polygonal_boss_dropped",
133
+ "polygonal_stock_dropped",
134
+ "polygonal_stock_length_dropped",
132
135
  "pmi_not_extracted",
133
136
  "placement_unsatisfiable",
134
137
  "pmi_dropped",
@@ -2876,6 +2879,14 @@ class Drawing:
2876
2879
  omissions=self._build.omissions,
2877
2880
  assembly=self.assembly,
2878
2881
  )
2882
+ issues += lint_polygonal_stock_coverage(
2883
+ self.part,
2884
+ recognition=recognition,
2885
+ features=getattr(model, "features", ()) if model is not None else (),
2886
+ registry=self._registry,
2887
+ omissions=self._build.omissions,
2888
+ assembly=self.assembly,
2889
+ )
2879
2890
  # Reverse direction (#487): a DECLARED feature with no matching geometry (a stale
2880
2891
  # phantom callout). Only for a caller-supplied model — detection can't over-declare.
2881
2892
  # _part_model is typed `object` (deliberately loose, #397); read features duck-typed.
@@ -36,6 +36,7 @@ from draftwright.linting.pmi_coverage import (
36
36
  lint_pmi_rendering,
37
37
  pmi_stage_summary,
38
38
  )
39
+ from draftwright.linting.polygonal_stock_coverage import lint_polygonal_stock_coverage
39
40
  from draftwright.linting.profiled_bore_coverage import lint_profiled_bore_coverage
40
41
  from draftwright.linting.slot_coverage import lint_slot_coverage
41
42
  from draftwright.linting.structural import lint_drawing
@@ -44,6 +45,7 @@ from draftwright.linting.suggest import _suggest_fix
44
45
  __all__ = [
45
46
  "CoverageState",
46
47
  "LintIssue",
48
+ "lint_polygonal_stock_coverage",
47
49
  "_suggest_fix",
48
50
  "lint_axial_coverage",
49
51
  "lint_boss_height_coverage",
@@ -0,0 +1,114 @@
1
+ """Semantic completeness for whole-part polygonal stock (#1082)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Literal
7
+
8
+ from draftwright.linting.issues import LintIssue
9
+ from draftwright.recognition import RecognitionResult
10
+
11
+ PolygonalStockState = Literal["placed", "suppressed", "dropped", "missing", "unverifiable"]
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class PolygonalStockOutcome:
16
+ parameter_id: str
17
+ state: PolygonalStockState
18
+
19
+
20
+ def _rounded(value) -> float:
21
+ return round(float(value), 3)
22
+
23
+
24
+ def _source_key(stock) -> tuple:
25
+ return (
26
+ stock.axis,
27
+ tuple(_rounded(value) for value in stock.center),
28
+ stock.side_count,
29
+ _rounded(stock.across_flats),
30
+ _rounded(stock.length),
31
+ )
32
+
33
+
34
+ def _feature_key(feature) -> tuple:
35
+ return (
36
+ feature.frame.axis,
37
+ tuple(_rounded(value) for value in feature.frame.origin),
38
+ feature.side_count,
39
+ _rounded(feature.across_flats),
40
+ _rounded(feature.length),
41
+ )
42
+
43
+
44
+ def polygonal_stock_outcomes(recognition, features, registry, omissions=()):
45
+ """Join each geometric stock requirement to its compiler/placement outcome."""
46
+ if recognition is None:
47
+ return []
48
+ if not isinstance(recognition, RecognitionResult):
49
+ raise TypeError("polygonal_stock_outcomes() requires the run's RecognitionResult")
50
+ by_key: dict[tuple, list] = {}
51
+ for feature in features:
52
+ if getattr(feature, "kind", None) == "polygonal_stock":
53
+ by_key.setdefault(_feature_key(feature), []).append(feature)
54
+ placed = {
55
+ measurement for name in registry.names() for measurement in registry.measurement_of(name)
56
+ }
57
+ suppressed = {
58
+ (omission.feature, omission.parameter_id)
59
+ for omission in omissions
60
+ if omission.feature is not None and omission.authored
61
+ }
62
+ dropped = {
63
+ measurement
64
+ for issue in registry.issues
65
+ for measurement in getattr(issue, "measurement_ids", ())
66
+ }
67
+ outcomes = []
68
+ for source in recognition.polygonal_stock:
69
+ matches = by_key.get(_source_key(source), ())
70
+ feature = matches[0] if len(matches) == 1 else None
71
+ for parameter in ("polygon_across_flats.length", "stock_length.length"):
72
+ if feature is None:
73
+ state: PolygonalStockState = "unverifiable"
74
+ elif any(
75
+ measurement.feature == feature and measurement.parameter == parameter
76
+ for measurement in placed
77
+ ):
78
+ state = "placed"
79
+ elif (feature, parameter) in suppressed:
80
+ state = "suppressed"
81
+ elif any(
82
+ measurement.feature == feature and measurement.parameter == parameter
83
+ for measurement in dropped
84
+ ):
85
+ state = "dropped"
86
+ else:
87
+ state = "missing"
88
+ outcomes.append(PolygonalStockOutcome(parameter, state))
89
+ return outcomes
90
+
91
+
92
+ def lint_polygonal_stock_coverage(
93
+ part, *, recognition, features, registry, omissions=(), assembly=None
94
+ ) -> list[LintIssue]:
95
+ """Require A/F and axial length, without duplicating an explicit placement drop."""
96
+ if assembly is None:
97
+ assembly = len(part.solids()) > 1
98
+ messages = {
99
+ "suppressed": "was deliberately omitted by the authored dimension set",
100
+ "missing": "has no placed, suppressed, or dropped measurement outcome",
101
+ "unverifiable": "cannot be joined to imported geometry without guessing",
102
+ }
103
+ issues = []
104
+ for outcome in polygonal_stock_outcomes(recognition, features, registry, omissions):
105
+ if outcome.state in {"placed", "dropped"}:
106
+ continue
107
+ issues.append(
108
+ LintIssue(
109
+ severity="info" if assembly else "warning",
110
+ code=f"polygonal_stock_requirement_{outcome.state}",
111
+ message=f"polygonal stock {outcome.parameter_id} {messages[outcome.state]}",
112
+ )
113
+ )
114
+ return issues
@@ -43,6 +43,7 @@ from draftwright.model.declare import (
43
43
  pocket,
44
44
  pocket_pattern,
45
45
  polygonal_boss,
46
+ polygonal_stock,
46
47
  rotational,
47
48
  slot,
48
49
  slot_pattern,
@@ -78,6 +79,7 @@ from draftwright.model.ir import (
78
79
  PocketFeature,
79
80
  PocketPatternFeature,
80
81
  PolygonalBossFeature,
82
+ PolygonalStockFeature,
81
83
  RotationalFeature,
82
84
  SlotFeature,
83
85
  SlotPatternFeature,
@@ -131,6 +133,7 @@ __all__ = [
131
133
  "StepFeature",
132
134
  "PlateFeature",
133
135
  "PolygonalBossFeature",
136
+ "PolygonalStockFeature",
134
137
  "StepLevelFeature",
135
138
  "measured_dimension",
136
139
  "build_part_model",
@@ -155,6 +158,7 @@ __all__ = [
155
158
  "pocket",
156
159
  "pocket_pattern",
157
160
  "polygonal_boss",
161
+ "polygonal_stock",
158
162
  "slot",
159
163
  "slot_pattern",
160
164
  "step",
@@ -60,6 +60,7 @@ from draftwright.model.ir import (
60
60
  PartModel,
61
61
  PocketFeature,
62
62
  Point,
63
+ PolygonalStockFeature,
63
64
  RotationalFeature,
64
65
  SlotFeature,
65
66
  SlotPatternFeature,
@@ -254,6 +255,7 @@ _FACTS: dict[str, tuple[str, ...]] = {
254
255
  "pad": ("frame", "width_axis", "long_axis"),
255
256
  "boss": ("frame", "thread"),
256
257
  "polygonal_boss": ("frame", "side_count", "flat_directions", "flat_centres"),
258
+ "polygonal_stock": ("frame", "side_count", "flat_directions", "flat_centres"),
257
259
  "step": ("frame", "thread"),
258
260
  "step_level": ("frame",),
259
261
  "envelope": ("frame",),
@@ -806,6 +808,11 @@ def _compile_overall_height(
806
808
  `.envelope()` — has no parameter naming its height, and the value falls back to the
807
809
  bounding box. The COMPILER may do that; a renderer may not, which is the whole point.
808
810
  """
811
+ # Whole polygonal stock already owns the same axial extent as `stock_length.length`.
812
+ # Letting the bbox fallback add `dim_height` would state one physical requirement twice,
813
+ # and would make authored suppression ineffective through an unrelated synthetic value.
814
+ if any(isinstance(feature, PolygonalStockFeature) for feature in model.features):
815
+ return None, None, []
809
816
  env = next((f for f in model.features if isinstance(f, EnvelopeFeature)), None)
810
817
  bb: Any = model.bbox # build123d BoundBox
811
818
  rot = next((f for f in model.features if isinstance(f, RotationalFeature)), None)
@@ -55,6 +55,7 @@ from draftwright.model.ir import (
55
55
  PocketPatternFeature,
56
56
  Point,
57
57
  PolygonalBossFeature,
58
+ PolygonalStockFeature,
58
59
  RotationalFeature,
59
60
  SlotFeature,
60
61
  SlotPatternFeature,
@@ -411,6 +412,34 @@ def polygonal_boss(
411
412
  )
412
413
 
413
414
 
415
+ def polygonal_stock(
416
+ *,
417
+ side_count,
418
+ across_flats,
419
+ length,
420
+ at,
421
+ axis,
422
+ flat_directions,
423
+ flat_centres,
424
+ span=None,
425
+ ) -> PolygonalStockFeature:
426
+ """Declare whole regular polygonal-prism stock by its manufacturing definition."""
427
+ axis = _norm_axis(axis)
428
+ _require_positive(across_flats=across_flats, length=length)
429
+ _require_point("at", at)
430
+ if span is None:
431
+ span = _span(at, axis, length)
432
+ return PolygonalStockFeature(
433
+ frame=Frame(tuple(at), axis),
434
+ side_count=side_count,
435
+ across_flats=across_flats,
436
+ length=length,
437
+ span=span,
438
+ flat_directions=flat_directions,
439
+ flat_centres=flat_centres,
440
+ )
441
+
442
+
414
443
  def step(
415
444
  obj=None, *, diameter=None, length=None, at=None, axis=None, span=None, thread=None
416
445
  ) -> StepFeature:
@@ -44,6 +44,7 @@ from draftwright.model.ir import (
44
44
  PocketFeature,
45
45
  PocketPatternFeature,
46
46
  PolygonalBossFeature,
47
+ PolygonalStockFeature,
47
48
  RotationalFeature,
48
49
  SlotFeature,
49
50
  SlotPatternFeature,
@@ -69,6 +70,7 @@ from draftwright.recognition import (
69
70
  PocketArray,
70
71
  PocketGrid,
71
72
  PolygonalBoss,
73
+ PolygonalStock,
72
74
  RaisedPad,
73
75
  RectGrid,
74
76
  RiserEvidence,
@@ -94,6 +96,7 @@ from draftwright.recognition import (
94
96
  recognise_pocket_patterns,
95
97
  recognise_pockets,
96
98
  recognise_polygonal_bosses,
99
+ recognise_polygonal_stock,
97
100
  recognise_rectangular_pads,
98
101
  recognise_risers,
99
102
  recognise_slot_patterns,
@@ -581,6 +584,24 @@ def _convert_polygonal_boss(boss: PolygonalBoss, ctx: ConvContext) -> PolygonalB
581
584
  )
582
585
 
583
586
 
587
+ def _convert_polygonal_stock(stock: PolygonalStock, ctx: ConvContext) -> PolygonalStockFeature:
588
+ centre = stock.center
589
+ lo = list(centre)
590
+ hi = list(centre)
591
+ axis_index = "xyz".index(stock.axis)
592
+ lo[axis_index] = stock.base
593
+ hi[axis_index] = stock.top
594
+ return PolygonalStockFeature(
595
+ frame=Frame(origin=centre, axis=stock.axis),
596
+ side_count=stock.side_count,
597
+ across_flats=stock.across_flats,
598
+ length=stock.length,
599
+ span=((lo[0], lo[1], lo[2]), (hi[0], hi[1], hi[2])),
600
+ flat_directions=stock.flat_directions,
601
+ flat_centres=stock.flat_centres,
602
+ )
603
+
604
+
584
605
  def _convert_plate(pl: Plate, ctx: ConvContext) -> PlateFeature:
585
606
  c = ctx.bbox.center()
586
607
  return PlateFeature(
@@ -643,6 +664,7 @@ _CONVERTERS: dict[type, Converter] = {
643
664
  TurnedStep: _convert_step,
644
665
  BossRecord: _convert_boss,
645
666
  PolygonalBoss: _convert_polygonal_boss,
667
+ PolygonalStock: _convert_polygonal_stock,
646
668
  Plate: _convert_plate,
647
669
  Chamfer: _convert_chamfer,
648
670
  Fillet: _convert_fillet,
@@ -700,6 +722,7 @@ def build_part_model(
700
722
  patterns=None,
701
723
  bosses=None,
702
724
  polygonal_bosses=None,
725
+ polygonal_stock=None,
703
726
  channels=None,
704
727
  slots=None,
705
728
  slot_patterns=None,
@@ -860,6 +883,12 @@ def build_part_model(
860
883
  polygonal_bosses = recognise_polygonal_bosses(part)
861
884
  features.extend(convert(boss, ctx) for boss in polygonal_bosses)
862
885
 
886
+ # A whole regular polygonal prism is stock, not a boss: it owns the form/A-F
887
+ # definition and its axial stock length independently of attachment evidence.
888
+ if polygonal_stock is None:
889
+ polygonal_stock = recognise_polygonal_stock(part)
890
+ features.extend(convert(stock, ctx) for stock in polygonal_stock)
891
+
863
892
  # Turned / circlip grooves (#148c) — recognised up front so the turned-step chain can
864
893
  # exclude any band a groove already dimensions: a groove floor is an annular band, and
865
894
  # its two walls read as shoulders, so recognise_turned_steps also delimits it as a
@@ -914,7 +943,7 @@ def build_part_model(
914
943
  # Overall envelope dims for a *prismatic* part — not a round single-OD body
915
944
  # (a boss whose diameter fills the footprint is the body, dimensioned by its
916
945
  # OD, not a box).
917
- if not _is_round(bbox, bosses_d):
946
+ if not _is_round(bbox, bosses_d) and not polygonal_stock:
918
947
  # The same construction the declared verb and the emitter's synthesis use.
919
948
  # Detection was the REFERENCE the other two were fixed to match (#977/#976); with
920
949
  # three independent producers, "matches the detector" was a property to re-verify