draftwright 0.4.1__tar.gz → 0.4.2__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 (208) hide show
  1. {draftwright-0.4.1 → draftwright-0.4.2}/.gitignore +1 -0
  2. {draftwright-0.4.1 → draftwright-0.4.2}/CHANGELOG.md +60 -0
  3. {draftwright-0.4.1 → draftwright-0.4.2}/PKG-INFO +7 -1
  4. {draftwright-0.4.1 → draftwright-0.4.2}/README.md +3 -0
  5. {draftwright-0.4.1 → draftwright-0.4.2}/pyproject.toml +11 -1
  6. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/__init__.py +13 -1
  7. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/_core.py +14 -1
  8. draftwright-0.4.2/src/draftwright/_pmi_part21.py +421 -0
  9. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/analysis.py +20 -8
  10. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/annotations/_common.py +12 -2
  11. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/annotations/from_model.py +292 -100
  12. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/annotations/orchestrator.py +25 -2
  13. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/audit.py +14 -7
  14. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/builder.py +10 -3
  15. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/cli.py +11 -10
  16. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/compose.py +1 -1
  17. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/drawing.py +50 -7
  18. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/export.py +57 -1
  19. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/linting/__init__.py +12 -0
  20. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/linting/coverage.py +37 -1
  21. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/linting/issues.py +4 -0
  22. draftwright-0.4.2/src/draftwright/linting/pmi_coverage.py +210 -0
  23. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/model/__init__.py +8 -0
  24. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/model/compiled.py +133 -46
  25. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/model/declare.py +60 -3
  26. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/model/detect.py +97 -16
  27. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/model/ir.py +209 -6
  28. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/model/planner.py +1 -0
  29. draftwright-0.4.2/src/draftwright/pmi.py +1187 -0
  30. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/recognition/__init__.py +3 -0
  31. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/recognition/pads.py +17 -7
  32. draftwright-0.4.2/src/draftwright/recognition/polygonal_bosses.py +272 -0
  33. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/recognition/result.py +4 -0
  34. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/recognition/slots.py +67 -23
  35. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/sheet.py +65 -6
  36. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/sheet_emit.py +375 -40
  37. {draftwright-0.4.1 → draftwright-0.4.2}/tests/refactor_golden/grooved_shaft.json +19 -1
  38. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_add_dimension.py +33 -1
  39. draftwright-0.4.2/tests/test_api_docs.py +31 -0
  40. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_audit_differential.py +28 -3
  41. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_compiled_plan_boundary.py +1 -0
  42. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_declare.py +16 -0
  43. draftwright-0.4.2/tests/test_export_dxf_curves.py +135 -0
  44. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_gdt_placement.py +96 -0
  45. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_import_boundaries.py +2 -0
  46. draftwright-0.4.2/tests/test_issue_1000_envelope_reuse.py +90 -0
  47. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_issue_1058_wheel_profile.py +281 -1
  48. draftwright-0.4.2/tests/test_issue_676_polygonal_boss.py +285 -0
  49. draftwright-0.4.2/tests/test_issue_676_polygonal_boss_declare.py +328 -0
  50. draftwright-0.4.2/tests/test_issue_955_height_contingency.py +195 -0
  51. draftwright-0.4.2/tests/test_issue_958_cross_solid_recognition.py +150 -0
  52. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_make_drawing.py +55 -3
  53. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_parameter_id.py +27 -0
  54. draftwright-0.4.2/tests/test_pmi.py +1451 -0
  55. draftwright-0.4.2/tests/test_pmi_datum_lowering.py +463 -0
  56. draftwright-0.4.2/tests/test_pmi_gtol_lowering.py +244 -0
  57. draftwright-0.4.2/tests/test_pmi_part21.py +473 -0
  58. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_recogniser_contract.py +11 -0
  59. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_recognition_manifest.py +8 -1
  60. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_recognition_result.py +4 -0
  61. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_render_seam.py +20 -2
  62. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_sheet_emit.py +571 -35
  63. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_sheet_identity_invariant.py +1 -0
  64. draftwright-0.4.2/tests/test_workflows.py +70 -0
  65. draftwright-0.4.1/src/draftwright/pmi.py +0 -363
  66. draftwright-0.4.1/tests/test_pmi.py +0 -248
  67. {draftwright-0.4.1 → draftwright-0.4.2}/LICENSE +0 -0
  68. {draftwright-0.4.1 → draftwright-0.4.2}/docs/adr/README.md +0 -0
  69. {draftwright-0.4.1 → draftwright-0.4.2}/skills/SKILL.md +0 -0
  70. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/_geometry.py +0 -0
  71. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/_warnings.py +0 -0
  72. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/annotate.py +0 -0
  73. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/annotations/__init__.py +0 -0
  74. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/annotations/balloons.py +0 -0
  75. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/annotations/holes.py +0 -0
  76. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/annotations/sections.py +0 -0
  77. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/fits.py +0 -0
  78. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
  79. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
  80. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
  81. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
  82. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/fonts/__init__.py +0 -0
  83. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/intents.py +0 -0
  84. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/layout.py +0 -0
  85. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/linting/channel_coverage.py +0 -0
  86. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/linting/flat_coverage.py +0 -0
  87. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/linting/profiled_bore_coverage.py +0 -0
  88. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/linting/slot_coverage.py +0 -0
  89. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/linting/structural.py +0 -0
  90. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/linting/suggest.py +0 -0
  91. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/make_drawing.py +0 -0
  92. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/model/callout.py +0 -0
  93. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/projection.py +0 -0
  94. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/py.typed +0 -0
  95. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/recognition/_features.py +0 -0
  96. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/recognition/_record.py +0 -0
  97. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/recognition/chamfers.py +0 -0
  98. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/recognition/countersinks.py +0 -0
  99. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/recognition/fillets.py +0 -0
  100. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/recognition/flats.py +0 -0
  101. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/recognition/grooves.py +0 -0
  102. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/recognition/levels.py +0 -0
  103. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/recognition/plates.py +0 -0
  104. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/recognition/profiled_bores.py +0 -0
  105. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/recognition/turned.py +0 -0
  106. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/registry.py +0 -0
  107. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/repair.py +0 -0
  108. {draftwright-0.4.1 → draftwright-0.4.2}/src/draftwright/score.py +0 -0
  109. {draftwright-0.4.1 → draftwright-0.4.2}/tests/_kernel.py +0 -0
  110. {draftwright-0.4.1 → draftwright-0.4.2}/tests/_layout_sig.py +0 -0
  111. {draftwright-0.4.1 → draftwright-0.4.2}/tests/conftest.py +0 -0
  112. {draftwright-0.4.1 → draftwright-0.4.2}/tests/fixtures/grm03_thumbwheel_drive_screw.step +0 -0
  113. {draftwright-0.4.1 → draftwright-0.4.2}/tests/fixtures/if_step_flat_across_cylinder.step +0 -0
  114. {draftwright-0.4.1 → draftwright-0.4.2}/tests/fixtures/issue_1058_wheel_rh.step +0 -0
  115. {draftwright-0.4.1 → draftwright-0.4.2}/tests/fixtures/issue_909_basic_part_design_017_body.step +0 -0
  116. {draftwright-0.4.1 → draftwright-0.4.2}/tests/fixtures/issue_915_case_study_2.step +0 -0
  117. {draftwright-0.4.1 → draftwright-0.4.2}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
  118. {draftwright-0.4.1 → draftwright-0.4.2}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
  119. {draftwright-0.4.1 → draftwright-0.4.2}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
  120. {draftwright-0.4.1 → draftwright-0.4.2}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
  121. {draftwright-0.4.1 → draftwright-0.4.2}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
  122. {draftwright-0.4.1 → draftwright-0.4.2}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
  123. {draftwright-0.4.1 → draftwright-0.4.2}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
  124. {draftwright-0.4.1 → draftwright-0.4.2}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
  125. {draftwright-0.4.1 → draftwright-0.4.2}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
  126. {draftwright-0.4.1 → draftwright-0.4.2}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
  127. {draftwright-0.4.1 → draftwright-0.4.2}/tests/fixtures/tuner_jig_blind_obround_pockets.step +0 -0
  128. {draftwright-0.4.1 → draftwright-0.4.2}/tests/refactor_golden/bracket_section.json +0 -0
  129. {draftwright-0.4.1 → draftwright-0.4.2}/tests/refactor_golden/centered_rebate.json +0 -0
  130. {draftwright-0.4.1 → draftwright-0.4.2}/tests/refactor_golden/chamfered.json +0 -0
  131. {draftwright-0.4.1 → draftwright-0.4.2}/tests/refactor_golden/filleted.json +0 -0
  132. {draftwright-0.4.1 → draftwright-0.4.2}/tests/refactor_golden/flange_dense.json +0 -0
  133. {draftwright-0.4.1 → draftwright-0.4.2}/tests/refactor_golden/grid_plate.json +0 -0
  134. {draftwright-0.4.1 → draftwright-0.4.2}/tests/refactor_golden/hex_bar.json +0 -0
  135. {draftwright-0.4.1 → draftwright-0.4.2}/tests/refactor_golden/holed_slot.json +0 -0
  136. {draftwright-0.4.1 → draftwright-0.4.2}/tests/refactor_golden/pocketed.json +0 -0
  137. {draftwright-0.4.1 → draftwright-0.4.2}/tests/refactor_golden/prismatic_ladder.json +0 -0
  138. {draftwright-0.4.1 → draftwright-0.4.2}/tests/refactor_golden/scattered_plate.json +0 -0
  139. {draftwright-0.4.1 → draftwright-0.4.2}/tests/refactor_golden/side_drilled.json +0 -0
  140. {draftwright-0.4.1 → draftwright-0.4.2}/tests/refactor_golden/turned_stepped.json +0 -0
  141. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_agents_guide.py +0 -0
  142. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_architecture_docs.py +0 -0
  143. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_balloon_ring_standoff.py +0 -0
  144. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_carve_free_position_callers.py +0 -0
  145. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_counting_calls.py +0 -0
  146. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_declared_recognition_gate.py +0 -0
  147. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_dense_sheet_canary.py +0 -0
  148. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_deprecation_dates.py +0 -0
  149. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_detect_once.py +0 -0
  150. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_detect_registry.py +0 -0
  151. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_dimension_role_vocabulary.py +0 -0
  152. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_drawing_encapsulation.py +0 -0
  153. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_e2e_slice.py +0 -0
  154. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_e2e_standards.py +0 -0
  155. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_export_dxf_zoom.py +0 -0
  156. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_fillets_adjacency.py +0 -0
  157. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_fits.py +0 -0
  158. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_flat_completeness.py +0 -0
  159. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_flat_stock_identity.py +0 -0
  160. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_flat_stock_opposition.py +0 -0
  161. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_grid_lattice_convention.py +0 -0
  162. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_issue_885_prismatic_coverage.py +0 -0
  163. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_issue_909_sloped_profile.py +0 -0
  164. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_issue_915_dense_case.py +0 -0
  165. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_issue_916_pocket_leader.py +0 -0
  166. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_issue_917_open_channel.py +0 -0
  167. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_label_provenance.py +0 -0
  168. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_layout.py +0 -0
  169. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_layout_cleanliness.py +0 -0
  170. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_layout_hypothesis.py +0 -0
  171. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_layout_property.py +0 -0
  172. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_lint_box_cache.py +0 -0
  173. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_lint_reconciliation.py +0 -0
  174. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_lint_structural.py +0 -0
  175. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_linting.py +0 -0
  176. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_location_vocabulary.py +0 -0
  177. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_note_verb.py +0 -0
  178. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_object_aspects.py +0 -0
  179. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_occupancy_boxes.py +0 -0
  180. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_part_model.py +0 -0
  181. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_pitch_dim_footprint.py +0 -0
  182. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_pocket_pattern.py +0 -0
  183. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_pocket_pattern_recognition.py +0 -0
  184. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_principal_profile_classifier.py +0 -0
  185. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_private_test_attr_reads.py +0 -0
  186. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_private_test_imports.py +0 -0
  187. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_recognition.py +0 -0
  188. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_refactor_golden.py +0 -0
  189. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_registry.py +0 -0
  190. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_score.py +0 -0
  191. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_script_detail_parity.py +0 -0
  192. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_sheet_gdt.py +0 -0
  193. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_sheet_notes.py +0 -0
  194. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_sheet_of.py +0 -0
  195. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_sheet_section.py +0 -0
  196. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_sheet_tables.py +0 -0
  197. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_slanted_blind_step.py +0 -0
  198. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_slot_completeness.py +0 -0
  199. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_slot_pattern.py +0 -0
  200. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_slot_pattern_recognition.py +0 -0
  201. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_soft_deprecation.py +0 -0
  202. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_solve_trace.py +0 -0
  203. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_strip_layout.py +0 -0
  204. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_suppression_ledger.py +0 -0
  205. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_suppression_marks.py +0 -0
  206. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_tolerances.py +0 -0
  207. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_turned_steps.py +0 -0
  208. {draftwright-0.4.1 → draftwright-0.4.2}/tests/test_witness_label_reconciliation.py +0 -0
@@ -12,4 +12,5 @@ testenv/
12
12
  coverage.xml
13
13
  coverage*.json
14
14
  htmlcov/
15
+ site/
15
16
  .DS_Store
@@ -2,6 +2,66 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## v0.4.2 — 2026-08-09
6
+
7
+ **The trustworthy manufacturing drawings release.** AP242 PMI is now accounted for from its
8
+ source records through lowering and rendering, imported datum faces retain an auditable topology
9
+ identity, and several drawing paths that previously omitted, invented, or stalled on defining
10
+ geometry now fail closed or produce the required annotation.
11
+
12
+ ### Added
13
+
14
+ - **AP242 geometric tolerances and datum features survive as source-backed manufacturing
15
+ requirements** (#62/#675/#1094/#1095/#1099/#1100). The importer recovers range-encoded
16
+ dimensional tolerances, tolerance characteristics, magnitudes, datum chains, and supported
17
+ all-around/all-over modifiers. Datum representation-item faces are resolved to imported
18
+ topology through OCCT's exact transfer relationship rather than geometric resemblance, so the
19
+ NIST CTC-01 datum definitions `A`, `B`, and `C` lower and render without guessing.
20
+
21
+ - **PMI completeness lint reconciles every AP242 source record through extraction, projection,
22
+ lowering, and rendering** (#623). Unsupported or incomplete requirements remain explicit with
23
+ source-specific outcomes instead of allowing a partial PMI transcription to report clean.
24
+
25
+ - **Hexagonal and other regular polygonal bosses are recognised and dimensioned by their
26
+ across-flats size** (#676). Automatic and declared paths share the same feature model, and
27
+ mutation tests reject incomplete, unequal, non-planar, and ambiguous face sets.
28
+
29
+ - **Generated `Sheet` scripts preserve references to named source objects where correspondence
30
+ is proven** (#1041). The `--script` path emits declarations such as
31
+ `sheet.hole(features.tap)` instead of copying detected numbers; ambiguous, stale, non-shape, or
32
+ unused mappings fail closed.
33
+
34
+ - **A curated API reference is published at
35
+ <https://pzfreo.github.io/draftwright/>** (#846), covering the supported entry points,
36
+ `Sheet` declaration surface and fluent handles, `Drawing` results, and feature constructors.
37
+
38
+ ### Fixed
39
+
40
+ - **Recognition no longer combines faces from different solids into phantom slots or pads**
41
+ (#958). Each physical solid is recognised independently before the results are combined.
42
+
43
+ - **`Sheet.from_part(...).envelope()` reuses the detected whole-part envelope** (#1000) instead
44
+ of appending a duplicate feature whose dimensions were then silently deduplicated on the page.
45
+
46
+ - **Crowded Z-turned step chains recover the independently approved overall height when the
47
+ chain cannot be placed** (#955), and placed step lengths retain measurement identity for
48
+ differential audit (#1004).
49
+
50
+ - **All-over GD&T leaders and complex STEP B-spline paths have bounded, export-safe DXF
51
+ conversion** (#1097/#1070). All-over scope now reaches the existing export-safe leader
52
+ representation, while spline conversion reads OCCT knots, poles, and weights through its
53
+ indexed API instead of expensive sequence wrappers; visible curve geometry is preserved.
54
+
55
+ ### Changed
56
+
57
+ - **Hosted CI retains every supported Python version and both dependency/kernel generations on
58
+ macOS and Windows with a smaller compatibility matrix** (#1103). PRs still require lint,
59
+ coverage, Codecov, and the compatibility gates; the slow standards tier runs once after merge.
60
+
61
+ - **The pinned wheel fixture now records mutation-gated evidence for a repeating 13-fold radial
62
+ profile without claiming gear semantics** (#1085). Runtime recognition remains fail-closed
63
+ until a separate feature contract has independently sufficient evidence.
64
+
5
65
  ## v0.4.1 — 2026-08-07
6
66
 
7
67
  **The recognition trust release.** Automatic drawings now fail closed on unsupported inner
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: draftwright
3
- Version: 0.4.1
3
+ Version: 0.4.2
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
7
7
  Project-URL: Issues, https://github.com/pzfreo/draftwright/issues
8
8
  Project-URL: Changelog, https://github.com/pzfreo/draftwright/blob/main/CHANGELOG.md
9
+ Project-URL: Documentation, https://pzfreo.github.io/draftwright/
9
10
  Author-email: Paul Fremantle <pzfreo@gmail.com>
10
11
  License: GNU AFFERO GENERAL PUBLIC LICENSE
11
12
  Version 3, 19 November 2007
@@ -682,6 +683,7 @@ Classifier: Programming Language :: Python :: 3.13
682
683
  Classifier: Programming Language :: Python :: 3.14
683
684
  Classifier: Topic :: Scientific/Engineering
684
685
  Requires-Python: <3.15,>=3.10
686
+ Requires-Dist: antlr4-python3-runtime<4.10
685
687
  Requires-Dist: build123d-drafting-helpers>=0.14.1
686
688
  Requires-Dist: build123d<0.11,>=0.9.0; python_full_version < '3.13'
687
689
  Requires-Dist: build123d<0.12,>=0.11; python_full_version >= '3.13'
@@ -689,6 +691,7 @@ Requires-Dist: numpy>=1.24
689
691
  Requires-Dist: pillow>=10
690
692
  Requires-Dist: pypdfium2>=4
691
693
  Requires-Dist: reportlab>=4.0
694
+ Requires-Dist: steputils==0.1
692
695
  Requires-Dist: svglib>=1.5
693
696
  Requires-Dist: typer>=0.12
694
697
  Requires-Dist: typing-extensions>=4.5; python_full_version < '3.13'
@@ -705,6 +708,9 @@ Description-Content-Type: text/markdown
705
708
  [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](LICENSE)
706
709
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
707
710
 
711
+ **[API reference](https://pzfreo.github.io/draftwright/)** ·
712
+ **[Examples and source](https://github.com/pzfreo/draftwright#readme)**
713
+
708
714
  Automated technical-drawing generation for [build123d](https://github.com/gumyr/build123d).
709
715
  Point it at a solid (or a STEP file) and get a fully-annotated multi-view engineering
710
716
  drawing — orthographic views, dimensions, section A–A, ISO hatching, title block — ready
@@ -9,6 +9,9 @@
9
9
  [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](LICENSE)
10
10
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
11
11
 
12
+ **[API reference](https://pzfreo.github.io/draftwright/)** ·
13
+ **[Examples and source](https://github.com/pzfreo/draftwright#readme)**
14
+
12
15
  Automated technical-drawing generation for [build123d](https://github.com/gumyr/build123d).
13
16
  Point it at a solid (or a STEP file) and get a fully-annotated multi-view engineering
14
17
  drawing — orthographic views, dimensions, section A–A, ISO hatching, title block — ready
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "draftwright"
7
- version = "0.4.1"
7
+ version = "0.4.2"
8
8
  description = "Automated technical-drawing generation for build123d"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -25,6 +25,11 @@ dependencies = [
25
25
  "pillow>=10",
26
26
  "pypdfium2>=4",
27
27
  "reportlab>=4.0",
28
+ # Structured ISO 10303-21 access for AP242 facts that OCCT's XCAF transfer drops (#62).
29
+ # STEPutils 0.1 is the only release; isolate it behind _pmi_part21 and pin its README's
30
+ # ANTLR compatibility constraint explicitly because the package metadata omits the cap.
31
+ "steputils==0.1",
32
+ "antlr4-python3-runtime<4.10",
28
33
  "svglib>=1.5",
29
34
  "typer>=0.12",
30
35
  # PEP 702 @deprecated: stdlib `warnings.deprecated` lands in 3.13; backport for 3.10–3.12.
@@ -53,6 +58,7 @@ Homepage = "https://github.com/pzfreo/draftwright"
53
58
  Repository = "https://github.com/pzfreo/draftwright"
54
59
  Issues = "https://github.com/pzfreo/draftwright/issues"
55
60
  Changelog = "https://github.com/pzfreo/draftwright/blob/main/CHANGELOG.md"
61
+ Documentation = "https://pzfreo.github.io/draftwright/"
56
62
 
57
63
  [tool.hatch.build.targets.wheel]
58
64
  packages = ["src/draftwright"]
@@ -144,6 +150,10 @@ select = ["E", "F", "I", "UP"]
144
150
  ignore = ["E501"]
145
151
 
146
152
  [dependency-groups]
153
+ docs = [
154
+ "mkdocs>=1.6,<2",
155
+ "mkdocstrings[python]>=0.30,<1",
156
+ ]
147
157
  dev = [
148
158
  "codespell>=2.3.0",
149
159
  "hypothesis>=6",
@@ -29,8 +29,11 @@ _LAZY = {
29
29
  "FeatureInfo": "draftwright.drawing",
30
30
  "Sheet": "draftwright.sheet",
31
31
  "lint_feature_coverage": "draftwright.linting",
32
+ "PmiExtractionReport": "draftwright.pmi",
32
33
  "PmiRecord": "draftwright.pmi",
34
+ "PmiSourceEntity": "draftwright.pmi",
33
35
  "extract_pmi": "draftwright.pmi",
36
+ "extract_pmi_report": "draftwright.pmi",
34
37
  "choose_scale": "draftwright.compose",
35
38
  # A warning category users are told to filter must be importable without reaching into a
36
39
  # private module (#1043 review) — and without paying for the CAD kernel. It lives in the
@@ -78,7 +81,13 @@ if TYPE_CHECKING: # static analysers / IDEs — no runtime import, no kernel co
78
81
  from draftwright.compose import choose_scale
79
82
  from draftwright.drawing import Drawing, FeatureInfo
80
83
  from draftwright.linting import lint_feature_coverage
81
- from draftwright.pmi import PmiRecord, extract_pmi
84
+ from draftwright.pmi import (
85
+ PmiExtractionReport,
86
+ PmiRecord,
87
+ PmiSourceEntity,
88
+ extract_pmi,
89
+ extract_pmi_report,
90
+ )
82
91
  from draftwright.sheet import Sheet
83
92
 
84
93
 
@@ -93,11 +102,14 @@ __all__ = [
93
102
  "Drawing",
94
103
  "SoftDeprecationWarning",
95
104
  "FeatureInfo",
105
+ "PmiExtractionReport",
96
106
  "PmiRecord",
107
+ "PmiSourceEntity",
97
108
  "Sheet",
98
109
  "build_drawing",
99
110
  "choose_scale",
100
111
  "extract_pmi",
112
+ "extract_pmi_report",
101
113
  "lint_feature_coverage",
102
114
  "make_drawing",
103
115
  ]
@@ -1015,8 +1015,16 @@ class Analysis:
1015
1015
  tolerance: str
1016
1016
  drawn_by: str
1017
1017
  out: str
1018
- pmi: list
1018
+ # Canonical AP242 source census + extraction outcomes for a STEP input, including off mode
1019
+ # (which inventories but does not lower records); None for an in-memory Shape. Typed as
1020
+ # object to keep the rank-1 core from importing rank-2 pmi; the compatibility `pmi` property
1021
+ # below is its records projection.
1022
+ pmi_report: object | None
1019
1023
  pmi_mode: str
1024
+ # True when the caller omitted `pmi` and the public default selected off. Kept separate
1025
+ # from the effective mode so the ignored-PMI diagnostic can distinguish that default from
1026
+ # an explicit opt-out without widening the three-mode renderer contract (#623).
1027
+ pmi_defaulted: bool
1020
1028
  # Standing ISO 7200 title-block fields (#766) — defaulted, so they sit after the
1021
1029
  # non-default fields above. Defaults preserve the prior output: revision "A", the rest
1022
1030
  # blank (the TitleBlock helper's own defaults).
@@ -1040,6 +1048,11 @@ class Analysis:
1040
1048
  # back to build_model(a).
1041
1049
  model: object | None = None
1042
1050
 
1051
+ @property
1052
+ def pmi(self) -> list:
1053
+ """Successful PMI records, derived from the canonical extraction report."""
1054
+ return list(getattr(self.pmi_report, "records", ()))
1055
+
1043
1056
 
1044
1057
  _greedy_strip_ys = _greedy_strip_1d
1045
1058
 
@@ -0,0 +1,421 @@
1
+ """Structured Part21 facts that OCCT's XCAF PMI transfer currently drops.
2
+
3
+ This is deliberately a leaf adapter: it understands ISO 10303-21 entities and units, but
4
+ knows nothing about XCAF labels, drafting IR, or placement. ``pmi.py`` owns the overlay and
5
+ may use a fact only after :func:`match_geometric_tolerance` proves exact correspondence.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import math
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+
14
+ from steputils import p21
15
+
16
+ _GTOL_ENTITY_KIND: dict[str, str] = {
17
+ "ANGULARITY_TOLERANCE": "angularity",
18
+ "CIRCULAR_RUNOUT_TOLERANCE": "circular_runout",
19
+ "ROUNDNESS_TOLERANCE": "circularity",
20
+ "COAXIALITY_TOLERANCE": "coaxiality",
21
+ "CONCENTRICITY_TOLERANCE": "concentricity",
22
+ "CYLINDRICITY_TOLERANCE": "cylindricity",
23
+ "FLATNESS_TOLERANCE": "flatness",
24
+ "PARALLELISM_TOLERANCE": "parallelism",
25
+ "PERPENDICULARITY_TOLERANCE": "perpendicularity",
26
+ "POSITION_TOLERANCE": "position",
27
+ "LINE_PROFILE_TOLERANCE": "profile_line",
28
+ "SURFACE_PROFILE_TOLERANCE": "profile_surface",
29
+ "STRAIGHTNESS_TOLERANCE": "straightness",
30
+ "SYMMETRY_TOLERANCE": "symmetry",
31
+ "TOTAL_RUNOUT_TOLERANCE": "total_runout",
32
+ }
33
+
34
+ # SI prefix → millimetres per prefixed metre. The mapping is definition-level standard
35
+ # data rather than a fixture inference; unsupported/non-SI units still fail closed.
36
+ _SI_METRE_TO_MM: dict[str, float] = {
37
+ "$": 1e3,
38
+ ".EXA.": 1e21,
39
+ ".PETA.": 1e18,
40
+ ".TERA.": 1e15,
41
+ ".GIGA.": 1e12,
42
+ ".MEGA.": 1e9,
43
+ ".KILO.": 1e6,
44
+ ".HECTO.": 1e5,
45
+ ".DECA.": 1e4,
46
+ ".DECI.": 1e2,
47
+ ".CENTI.": 1e1,
48
+ ".MILLI.": 1.0,
49
+ ".MICRO.": 1e-3,
50
+ ".NANO.": 1e-6,
51
+ ".PICO.": 1e-9,
52
+ ".FEMTO.": 1e-12,
53
+ ".ATTO.": 1e-15,
54
+ }
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class GeometricToleranceFact:
59
+ """One Part21 tolerance characteristic and its independently auditable magnitude."""
60
+
61
+ entity_id: str
62
+ semantic_name: str
63
+ kind: str
64
+ value_mm: float | None
65
+ reason: str = ""
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class DatumOccurrenceFact:
70
+ """One datum reference used by one Part21 geometric-tolerance context.
71
+
72
+ ``datum_feature_id`` identifies the authored physical feature; several occurrence facts
73
+ may therefore share it. ``reference_item_ids`` are the exact representation items bound
74
+ to that feature, retained for correspondence rather than interpreted as page geometry.
75
+ """
76
+
77
+ tolerance_id: str
78
+ tolerance_name: str
79
+ tolerance_kind: str
80
+ datum_feature_id: str
81
+ datum_id: str
82
+ letter: str
83
+ reference_item_ids: tuple[str, ...] = ()
84
+ reason: str = ""
85
+
86
+
87
+ def _entities(instance) -> tuple:
88
+ entity = getattr(instance, "entity", None)
89
+ if entity is not None:
90
+ return (entity,)
91
+ return tuple(instance.entities)
92
+
93
+
94
+ def _entity_named(instance, name: str):
95
+ return next((entity for entity in _entities(instance) if entity.name == name), None)
96
+
97
+
98
+ def _references(value) -> tuple[str, ...]:
99
+ """Return Part21 references nested in a parameter, preserving source order."""
100
+ if isinstance(value, p21.Reference):
101
+ return (str(value),)
102
+ if isinstance(value, p21.TypedParameter):
103
+ return _references(value.param)
104
+ if isinstance(value, (tuple, list)):
105
+ return tuple(ref for item in value for ref in _references(item))
106
+ return ()
107
+
108
+
109
+ def _instance_is(step, ref: str, name: str) -> bool:
110
+ instance = step.get(ref)
111
+ return instance is not None and _entity_named(instance, name) is not None
112
+
113
+
114
+ def _datum_feature_relationships(step) -> dict[str, tuple[str, tuple[str, ...]]]:
115
+ """Return ``datum id -> (letter, datum-feature ids)`` without hiding ambiguity."""
116
+ datums: dict[str, str] = {}
117
+ relationships: list[tuple[str, str]] = []
118
+ for section in step.data:
119
+ for entity_id, instance in section.instances.items():
120
+ datum = _entity_named(instance, "DATUM")
121
+ if datum is not None and len(datum.params) >= 5:
122
+ letter = datum.params[4]
123
+ datums[entity_id] = str(letter).strip() if isinstance(letter, str) else ""
124
+ relationship = _entity_named(instance, "SHAPE_ASPECT_RELATIONSHIP")
125
+ if relationship is not None and len(relationship.params) >= 4:
126
+ left, right = relationship.params[2:4]
127
+ if isinstance(left, p21.Reference) and isinstance(right, p21.Reference):
128
+ relationships.append((str(left), str(right)))
129
+
130
+ result: dict[str, list[str]] = {datum_id: [] for datum_id in datums}
131
+ for left, right in relationships:
132
+ if left in datums and _instance_is(step, right, "DATUM_FEATURE"):
133
+ result[left].append(right)
134
+ elif right in datums and _instance_is(step, left, "DATUM_FEATURE"):
135
+ result[right].append(left)
136
+ return {
137
+ datum_id: (datums[datum_id], tuple(dict.fromkeys(feature_ids)))
138
+ for datum_id, feature_ids in result.items()
139
+ }
140
+
141
+
142
+ def _datum_feature_items(step) -> dict[str, tuple[str, ...]]:
143
+ """Return exact representation-item references for each authored datum feature."""
144
+ result: dict[str, list[str]] = {}
145
+ usage_names = ("GEOMETRIC_ITEM_SPECIFIC_USAGE", "ITEM_IDENTIFIED_REPRESENTATION_USAGE")
146
+ for section in step.data:
147
+ for instance in section.instances.values():
148
+ for usage_name in usage_names:
149
+ usage = _entity_named(instance, usage_name)
150
+ if usage is None or len(usage.params) < 5:
151
+ continue
152
+ feature = usage.params[2]
153
+ if not isinstance(feature, p21.Reference) or not _instance_is(
154
+ step, str(feature), "DATUM_FEATURE"
155
+ ):
156
+ continue
157
+ result.setdefault(str(feature), []).extend(_references(usage.params[4]))
158
+ return {key: tuple(dict.fromkeys(values)) for key, values in result.items()}
159
+
160
+
161
+ def _unit_factor_mm(step, unit_ref: str) -> tuple[float | None, str]:
162
+ unit = step.get(unit_ref)
163
+ if unit is None:
164
+ return None, f"length unit {unit_ref} is missing"
165
+ si_unit = _entity_named(unit, "SI_UNIT")
166
+ if si_unit is None or len(si_unit.params) < 2:
167
+ return None, f"length unit {unit_ref} is not a supported SI metre unit"
168
+ prefix, unit_name = (str(param).upper() for param in si_unit.params[:2])
169
+ if unit_name != ".METRE." or prefix not in _SI_METRE_TO_MM:
170
+ return None, f"length unit {unit_ref} is not a supported SI metre unit"
171
+ return _SI_METRE_TO_MM[prefix], ""
172
+
173
+
174
+ def _length_value_mm(step, measure_ref: str) -> tuple[float | None, str]:
175
+ measure = step.get(measure_ref)
176
+ if measure is None:
177
+ return None, f"tolerance magnitude {measure_ref} is missing"
178
+ measure_with_unit = _entity_named(measure, "MEASURE_WITH_UNIT")
179
+ if measure_with_unit is None:
180
+ entity = next(
181
+ (
182
+ candidate
183
+ for candidate in _entities(measure)
184
+ if candidate.name.endswith("MEASURE_WITH_UNIT") and len(candidate.params) >= 2
185
+ ),
186
+ None,
187
+ )
188
+ measure_with_unit = entity
189
+ if measure_with_unit is None or len(measure_with_unit.params) < 2:
190
+ return None, f"tolerance magnitude {measure_ref} is not a measure with unit"
191
+
192
+ typed_value, unit_ref = measure_with_unit.params[:2]
193
+ if (
194
+ not isinstance(typed_value, p21.TypedParameter)
195
+ or typed_value.type_name != "LENGTH_MEASURE"
196
+ ):
197
+ return None, f"tolerance magnitude {measure_ref} is not a length measure"
198
+ if not isinstance(unit_ref, p21.Reference):
199
+ return None, f"tolerance magnitude {measure_ref} has no referenced length unit"
200
+ try:
201
+ value = float(typed_value.param)
202
+ except (TypeError, ValueError):
203
+ return None, f"tolerance magnitude {measure_ref} is not numeric"
204
+ if not math.isfinite(value) or value <= 0:
205
+ return None, f"tolerance magnitude {measure_ref} must be finite and positive"
206
+
207
+ factor, reason = _unit_factor_mm(step, str(unit_ref))
208
+ if factor is None:
209
+ return None, reason
210
+ return value * factor, ""
211
+
212
+
213
+ def read_geometric_tolerances(step_file: str | Path) -> tuple[GeometricToleranceFact, ...]:
214
+ """Read supported geometric-tolerance facts without inferring entity order.
215
+
216
+ Syntax/read failures propagate to the XCAF overlay, which records them as a partial
217
+ extraction reason rather than turning a broken Part21 pass into a report-level failure.
218
+ """
219
+
220
+ step = p21.readfile(step_file)
221
+ facts: list[GeometricToleranceFact] = []
222
+ for section in step.data:
223
+ for entity_id, instance in section.instances.items():
224
+ entities = _entities(instance)
225
+ characteristic_names = [
226
+ entity.name for entity in entities if entity.name in _GTOL_ENTITY_KIND
227
+ ]
228
+ if not characteristic_names:
229
+ continue
230
+ if len(characteristic_names) != 1:
231
+ facts.append(
232
+ GeometricToleranceFact(
233
+ entity_id,
234
+ "",
235
+ "",
236
+ None,
237
+ "geometric tolerance has multiple supported characteristics",
238
+ )
239
+ )
240
+ continue
241
+
242
+ characteristic_name = characteristic_names[0]
243
+ kind = _GTOL_ENTITY_KIND[characteristic_name]
244
+ base = _entity_named(instance, "GEOMETRIC_TOLERANCE")
245
+ if base is None:
246
+ base = _entity_named(instance, characteristic_name)
247
+ if base is None or len(base.params) < 3:
248
+ facts.append(
249
+ GeometricToleranceFact(
250
+ entity_id,
251
+ "",
252
+ kind,
253
+ None,
254
+ "geometric tolerance has no name/magnitude tuple",
255
+ )
256
+ )
257
+ continue
258
+
259
+ name_param, magnitude_param = base.params[0], base.params[2]
260
+ semantic_name = str(name_param).strip() if isinstance(name_param, str) else ""
261
+ if not isinstance(magnitude_param, p21.Reference):
262
+ facts.append(
263
+ GeometricToleranceFact(
264
+ entity_id,
265
+ semantic_name,
266
+ kind,
267
+ None,
268
+ "geometric tolerance has no referenced magnitude",
269
+ )
270
+ )
271
+ continue
272
+ value_mm, reason = _length_value_mm(step, str(magnitude_param))
273
+ facts.append(GeometricToleranceFact(entity_id, semantic_name, kind, value_mm, reason))
274
+ return tuple(facts)
275
+
276
+
277
+ def read_datum_occurrences(step_file: str | Path) -> tuple[DatumOccurrenceFact, ...]:
278
+ """Read exact datum-feature uses from Part21 without collapsing repeated occurrences."""
279
+ step = p21.readfile(step_file)
280
+ datum_features = _datum_feature_relationships(step)
281
+ feature_items = _datum_feature_items(step)
282
+ facts: list[DatumOccurrenceFact] = []
283
+
284
+ for section in step.data:
285
+ for tolerance_id, instance in section.instances.items():
286
+ entities = _entities(instance)
287
+ characteristic_names = [
288
+ entity.name for entity in entities if entity.name in _GTOL_ENTITY_KIND
289
+ ]
290
+ if len(characteristic_names) != 1:
291
+ continue
292
+ characteristic_name = characteristic_names[0]
293
+ kind = _GTOL_ENTITY_KIND[characteristic_name]
294
+ base = _entity_named(instance, "GEOMETRIC_TOLERANCE") or _entity_named(
295
+ instance, characteristic_name
296
+ )
297
+ if base is None or not base.params:
298
+ continue
299
+ name_param = base.params[0]
300
+ tolerance_name = str(name_param).strip() if isinstance(name_param, str) else ""
301
+
302
+ system_refs = tuple(
303
+ dict.fromkeys(
304
+ ref
305
+ for entity in entities
306
+ for param in entity.params
307
+ for ref in _references(param)
308
+ if _instance_is(step, ref, "DATUM_SYSTEM")
309
+ )
310
+ )
311
+ for system_ref in system_refs:
312
+ system = _entity_named(step.get(system_ref), "DATUM_SYSTEM")
313
+ compartment_refs = tuple(
314
+ ref
315
+ for param in system.params
316
+ for ref in _references(param)
317
+ if _instance_is(step, ref, "DATUM_REFERENCE_COMPARTMENT")
318
+ )
319
+ for compartment_ref in compartment_refs:
320
+ compartment = _entity_named(
321
+ step.get(compartment_ref), "DATUM_REFERENCE_COMPARTMENT"
322
+ )
323
+ datum_refs = tuple(
324
+ ref
325
+ for param in compartment.params
326
+ for ref in _references(param)
327
+ if _instance_is(step, ref, "DATUM")
328
+ )
329
+ if len(datum_refs) != 1:
330
+ facts.append(
331
+ DatumOccurrenceFact(
332
+ tolerance_id,
333
+ tolerance_name,
334
+ kind,
335
+ "",
336
+ "",
337
+ "",
338
+ reason=(
339
+ f"datum compartment {compartment_ref} has "
340
+ f"{len(datum_refs)} datum references"
341
+ ),
342
+ )
343
+ )
344
+ continue
345
+ datum_id = datum_refs[0]
346
+ letter, feature_ids = datum_features[datum_id]
347
+ if len(feature_ids) != 1:
348
+ facts.append(
349
+ DatumOccurrenceFact(
350
+ tolerance_id,
351
+ tolerance_name,
352
+ kind,
353
+ "",
354
+ datum_id,
355
+ letter,
356
+ reason=(
357
+ f"datum {datum_id} has {len(feature_ids)} related "
358
+ "DATUM_FEATUREs"
359
+ ),
360
+ )
361
+ )
362
+ continue
363
+ (feature_id,) = feature_ids
364
+ items = feature_items.get(feature_id, ())
365
+ facts.append(
366
+ DatumOccurrenceFact(
367
+ tolerance_id,
368
+ tolerance_name,
369
+ kind,
370
+ feature_id,
371
+ datum_id,
372
+ letter,
373
+ items,
374
+ ""
375
+ if items
376
+ else f"datum feature {feature_id} has no representation items",
377
+ )
378
+ )
379
+ return tuple(facts)
380
+
381
+
382
+ def match_datum_occurrence(
383
+ facts: tuple[DatumOccurrenceFact, ...], tolerance_name: str, letter: str
384
+ ) -> tuple[DatumOccurrenceFact | None, str]:
385
+ """Require one exact datum occurrence for an XCAF tolerance context and letter."""
386
+ name, datum_letter = tolerance_name.strip(), letter.strip()
387
+ if not name:
388
+ return None, "XCAF datum occurrence has no tolerance context"
389
+ if not datum_letter:
390
+ return None, "XCAF datum occurrence has no letter"
391
+ matches = [
392
+ fact for fact in facts if fact.tolerance_name == name and fact.letter == datum_letter
393
+ ]
394
+ if not matches:
395
+ return None, f"Part21 has no datum {datum_letter!r} in tolerance {name!r}"
396
+ if len(matches) > 1:
397
+ ids = ", ".join(f"{fact.tolerance_id}/{fact.datum_feature_id}" for fact in matches)
398
+ return (
399
+ None,
400
+ f"Part21 datum correspondence is ambiguous for {name!r}/{datum_letter!r} ({ids})",
401
+ )
402
+ fact = matches[0]
403
+ return fact, fact.reason
404
+
405
+
406
+ def match_geometric_tolerance(
407
+ facts: tuple[GeometricToleranceFact, ...], semantic_name: str, kind: str
408
+ ) -> tuple[GeometricToleranceFact | None, str]:
409
+ """Require one exact, nonblank ``(semantic name, kind)`` correspondence."""
410
+
411
+ name = semantic_name.strip()
412
+ if not name:
413
+ return None, "XCAF geometric tolerance has no semantic name"
414
+ matches = [fact for fact in facts if fact.semantic_name == name and fact.kind == kind]
415
+ if not matches:
416
+ return None, f"Part21 has no {kind} geometric tolerance named {name!r}"
417
+ if len(matches) > 1:
418
+ entity_ids = ", ".join(fact.entity_id for fact in matches)
419
+ return None, f"Part21 correspondence is ambiguous for {kind} {name!r} ({entity_ids})"
420
+ fact = matches[0]
421
+ return fact, fact.reason