draftwright 0.4.13__tar.gz → 0.4.14__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 (288) hide show
  1. {draftwright-0.4.13 → draftwright-0.4.14}/CHANGELOG.md +19 -0
  2. {draftwright-0.4.13 → draftwright-0.4.14}/PKG-INFO +2 -2
  3. {draftwright-0.4.13 → draftwright-0.4.14}/docs/reference/recogniser-capabilities.md +31 -0
  4. {draftwright-0.4.13 → draftwright-0.4.14}/pyproject.toml +2 -2
  5. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/_core.py +4 -1
  6. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/annotations/_common.py +386 -33
  7. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/annotations/from_model.py +16 -1
  8. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/builder.py +67 -26
  9. draftwright-0.4.14/src/draftwright/linting/ink_overlap.py +745 -0
  10. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/linting/quality.py +1 -0
  11. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/linting/structural.py +161 -13
  12. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/linting/suggest.py +7 -0
  13. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/repair.py +26 -0
  14. {draftwright-0.4.13 → draftwright-0.4.14}/tests/conftest.py +2 -0
  15. draftwright-0.4.14/tests/fixtures/grm03_thumbwheel_drive_screw_ap242_pmi.step +32991 -0
  16. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1166_cross_pass_feature_leaders.py +7 -1
  17. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1296_cylindrical_diameter_pmi.py +1 -2
  18. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1298_manufacturing_requirements.py +1 -3
  19. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1299_page_escalation.py +37 -13
  20. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1308_machined_leader_analytics.py +8 -0
  21. draftwright-0.4.14/tests/test_issue_1332_overlap_remedy.py +89 -0
  22. draftwright-0.4.14/tests/test_issue_1332_overlap_remedy_fast.py +106 -0
  23. draftwright-0.4.14/tests/test_issue_1332_zone_labels.py +49 -0
  24. draftwright-0.4.14/tests/test_issue_1334_prevent_ink.py +158 -0
  25. draftwright-0.4.14/tests/test_issue_1336_grm03_ap242_pmi_fixture.py +77 -0
  26. draftwright-0.4.14/tests/test_issue_1338_scale_before_page_escalation.py +103 -0
  27. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_443_grm03_axial_coverage.py +7 -4
  28. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_915_dense_case.py +62 -2
  29. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_916_pocket_leader.py +29 -1
  30. draftwright-0.4.14/tests/test_lint_ink_overlap.py +727 -0
  31. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_lint_structural.py +56 -0
  32. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_make_drawing.py +72 -4
  33. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_recogniser_capabilities.py +40 -0
  34. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_strip_layout.py +10 -2
  35. {draftwright-0.4.13 → draftwright-0.4.14}/.gitignore +0 -0
  36. {draftwright-0.4.13 → draftwright-0.4.14}/LICENSE +0 -0
  37. {draftwright-0.4.13 → draftwright-0.4.14}/README.md +0 -0
  38. {draftwright-0.4.13 → draftwright-0.4.14}/docs/adr/README.md +0 -0
  39. {draftwright-0.4.13 → draftwright-0.4.14}/docs/reference/sheet.md +0 -0
  40. {draftwright-0.4.13 → draftwright-0.4.14}/docs/research/1062-repeating-radial-profile-evidence.md +0 -0
  41. {draftwright-0.4.13 → draftwright-0.4.14}/skills/SKILL.md +0 -0
  42. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/__init__.py +0 -0
  43. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/_build_profile.py +0 -0
  44. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/_geometry.py +0 -0
  45. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/_pmi_part21.py +0 -0
  46. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/_warnings.py +0 -0
  47. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/analysis.py +0 -0
  48. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/annotate.py +0 -0
  49. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/annotations/__init__.py +0 -0
  50. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/annotations/balloons.py +0 -0
  51. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/annotations/gears.py +0 -0
  52. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/annotations/holes.py +0 -0
  53. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/annotations/leaders.py +0 -0
  54. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/annotations/orchestrator.py +0 -0
  55. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/annotations/sections.py +0 -0
  56. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/audit.py +0 -0
  57. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/cli.py +0 -0
  58. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/compose.py +0 -0
  59. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/drawing.py +0 -0
  60. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/evaluation/__init__.py +0 -0
  61. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/evaluation/step_analysis.py +0 -0
  62. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/export.py +0 -0
  63. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/fits.py +0 -0
  64. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
  65. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
  66. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
  67. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
  68. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/fonts/__init__.py +0 -0
  69. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/intents.py +0 -0
  70. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/layout.py +0 -0
  71. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/linting/__init__.py +0 -0
  72. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/linting/channel_coverage.py +0 -0
  73. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/linting/coverage.py +0 -0
  74. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/linting/evidence.py +0 -0
  75. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/linting/flat_coverage.py +0 -0
  76. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/linting/gear_coverage.py +0 -0
  77. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/linting/hole_coverage.py +0 -0
  78. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/linting/issues.py +0 -0
  79. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/linting/pmi_coverage.py +0 -0
  80. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/linting/polygonal_stock_coverage.py +0 -0
  81. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/linting/profiled_bore_coverage.py +0 -0
  82. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/linting/slot_coverage.py +0 -0
  83. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/make_drawing.py +0 -0
  84. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/model/__init__.py +0 -0
  85. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/model/callout.py +0 -0
  86. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/model/compiled.py +0 -0
  87. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/model/declare.py +0 -0
  88. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/model/detect.py +0 -0
  89. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/model/ir.py +0 -0
  90. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/model/planner.py +0 -0
  91. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/model/pmi_lowering.py +0 -0
  92. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/pmi.py +0 -0
  93. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/projection.py +0 -0
  94. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/py.typed +0 -0
  95. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/recogniser_contract.py +0 -0
  96. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/recognition/__init__.py +0 -0
  97. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/recognition_cache.py +0 -0
  98. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/registry.py +0 -0
  99. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/score.py +0 -0
  100. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/sheet.py +0 -0
  101. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/sheet_emit.py +0 -0
  102. {draftwright-0.4.13 → draftwright-0.4.14}/src/draftwright/view_plan.py +0 -0
  103. {draftwright-0.4.13 → draftwright-0.4.14}/tests/_kernel.py +0 -0
  104. {draftwright-0.4.13 → draftwright-0.4.14}/tests/_layout_sig.py +0 -0
  105. {draftwright-0.4.13 → draftwright-0.4.14}/tests/fixtures/ap242_single_cylinder_diameter.step +0 -0
  106. {draftwright-0.4.13 → draftwright-0.4.14}/tests/fixtures/evaluation/README.md +0 -0
  107. {draftwright-0.4.13 → draftwright-0.4.14}/tests/fixtures/evaluation/ambiguous-open-semicircle.step +0 -0
  108. {draftwright-0.4.13 → draftwright-0.4.14}/tests/fixtures/evaluation/blind-hole.step +0 -0
  109. {draftwright-0.4.13 → draftwright-0.4.14}/tests/fixtures/evaluation/corpus-v1.json +0 -0
  110. {draftwright-0.4.13 → draftwright-0.4.14}/tests/fixtures/evaluation/plain-block.step +0 -0
  111. {draftwright-0.4.13 → draftwright-0.4.14}/tests/fixtures/evaluation/topology-a.step +0 -0
  112. {draftwright-0.4.13 → draftwright-0.4.14}/tests/fixtures/evaluation/topology-b.step +0 -0
  113. {draftwright-0.4.13 → draftwright-0.4.14}/tests/fixtures/grm03_thumbwheel_drive_screw.step +0 -0
  114. {draftwright-0.4.13 → draftwright-0.4.14}/tests/fixtures/grm04_drive_plate.step +0 -0
  115. {draftwright-0.4.13 → draftwright-0.4.14}/tests/fixtures/if_step_flat_across_cylinder.step +0 -0
  116. {draftwright-0.4.13 → draftwright-0.4.14}/tests/fixtures/issue_1058_wheel_rh.step +0 -0
  117. {draftwright-0.4.13 → draftwright-0.4.14}/tests/fixtures/issue_909_basic_part_design_017_body.step +0 -0
  118. {draftwright-0.4.13 → draftwright-0.4.14}/tests/fixtures/issue_915_case_study_2.step +0 -0
  119. {draftwright-0.4.13 → draftwright-0.4.14}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
  120. {draftwright-0.4.13 → draftwright-0.4.14}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
  121. {draftwright-0.4.13 → draftwright-0.4.14}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
  122. {draftwright-0.4.13 → draftwright-0.4.14}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
  123. {draftwright-0.4.13 → draftwright-0.4.14}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
  124. {draftwright-0.4.13 → draftwright-0.4.14}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
  125. {draftwright-0.4.13 → draftwright-0.4.14}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
  126. {draftwright-0.4.13 → draftwright-0.4.14}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
  127. {draftwright-0.4.13 → draftwright-0.4.14}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
  128. {draftwright-0.4.13 → draftwright-0.4.14}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
  129. {draftwright-0.4.13 → draftwright-0.4.14}/tests/fixtures/tuner_jig_blind_obround_pockets.step +0 -0
  130. {draftwright-0.4.13 → draftwright-0.4.14}/tests/refactor_golden/bracket_section.json +0 -0
  131. {draftwright-0.4.13 → draftwright-0.4.14}/tests/refactor_golden/centered_rebate.json +0 -0
  132. {draftwright-0.4.13 → draftwright-0.4.14}/tests/refactor_golden/chamfered.json +0 -0
  133. {draftwright-0.4.13 → draftwright-0.4.14}/tests/refactor_golden/filleted.json +0 -0
  134. {draftwright-0.4.13 → draftwright-0.4.14}/tests/refactor_golden/flange_dense.json +0 -0
  135. {draftwright-0.4.13 → draftwright-0.4.14}/tests/refactor_golden/grid_plate.json +0 -0
  136. {draftwright-0.4.13 → draftwright-0.4.14}/tests/refactor_golden/grooved_shaft.json +0 -0
  137. {draftwright-0.4.13 → draftwright-0.4.14}/tests/refactor_golden/hex_bar.json +0 -0
  138. {draftwright-0.4.13 → draftwright-0.4.14}/tests/refactor_golden/holed_slot.json +0 -0
  139. {draftwright-0.4.13 → draftwright-0.4.14}/tests/refactor_golden/pocketed.json +0 -0
  140. {draftwright-0.4.13 → draftwright-0.4.14}/tests/refactor_golden/prismatic_ladder.json +0 -0
  141. {draftwright-0.4.13 → draftwright-0.4.14}/tests/refactor_golden/scattered_plate.json +0 -0
  142. {draftwright-0.4.13 → draftwright-0.4.14}/tests/refactor_golden/side_drilled.json +0 -0
  143. {draftwright-0.4.13 → draftwright-0.4.14}/tests/refactor_golden/turned_stepped.json +0 -0
  144. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_add_dimension.py +0 -0
  145. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_adr0018_arrangement_gate.py +0 -0
  146. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_adr0018_view_routing.py +0 -0
  147. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_adr0018_view_selection.py +0 -0
  148. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_agents_guide.py +0 -0
  149. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_api_docs.py +0 -0
  150. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_architecture_docs.py +0 -0
  151. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_audit_differential.py +0 -0
  152. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_balloon_ring_standoff.py +0 -0
  153. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_build_profile_harness.py +0 -0
  154. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_carve_free_position_callers.py +0 -0
  155. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_compiled_plan_boundary.py +0 -0
  156. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_counting_calls.py +0 -0
  157. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_declare.py +0 -0
  158. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_declared_recognition_gate.py +0 -0
  159. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_dense_sheet_canary.py +0 -0
  160. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_deprecation_dates.py +0 -0
  161. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_detect_once.py +0 -0
  162. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_detect_registry.py +0 -0
  163. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_dimension_role_vocabulary.py +0 -0
  164. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_drawing_encapsulation.py +0 -0
  165. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_e2e_slice.py +0 -0
  166. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_e2e_standards.py +0 -0
  167. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_export_dxf_curves.py +0 -0
  168. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_export_dxf_zoom.py +0 -0
  169. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_external_recognition_boundary.py +0 -0
  170. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_fillets_adjacency.py +0 -0
  171. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_fits.py +0 -0
  172. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_flat_completeness.py +0 -0
  173. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_flat_stock_identity.py +0 -0
  174. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_flat_stock_opposition.py +0 -0
  175. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_gdt_placement.py +0 -0
  176. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_grid_lattice_convention.py +0 -0
  177. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_import_boundaries.py +0 -0
  178. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_iso_nts_caption_placement.py +0 -0
  179. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1000_envelope_reuse.py +0 -0
  180. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1058_wheel_profile.py +0 -0
  181. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1073_cross_body_patterns.py +0 -0
  182. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1082_polygonal_stock.py +0 -0
  183. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1086_declared_gears.py +0 -0
  184. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1116_ap242_hole_tolerance_lowering.py +0 -0
  185. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1130_view_planning_evidence.py +0 -0
  186. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1142_hole_leader_labels.py +0 -0
  187. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1143_hole_completeness.py +0 -0
  188. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1144_transactional_hole_table.py +0 -0
  189. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1146_scale_completeness.py +0 -0
  190. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1147_legibility_pair_aggregation.py +0 -0
  191. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1153_contradictory_dimensions.py +0 -0
  192. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1154_one_owner_per_measurement.py +0 -0
  193. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1155_grm04_sheet_use.py +0 -0
  194. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1176_quality_fidelity.py +0 -0
  195. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1177_angular_dimensions.py +0 -0
  196. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1187_unroutable_leaders.py +0 -0
  197. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1188_per_view_assignment.py +0 -0
  198. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1190_section_decision.py +0 -0
  199. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1196_deterministic_view_names.py +0 -0
  200. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1202_observed_drawing_consumer.py +0 -0
  201. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1204_multiscale_view_issues.py +0 -0
  202. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1209_linear_pmi_witnesses.py +0 -0
  203. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1215_envelope_tolerance.py +0 -0
  204. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1215_no_approved_tolerance_is_dropped.py +0 -0
  205. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1216_callout_reservation_measures_ink.py +0 -0
  206. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1216_pairwise_across_scale_groups.py +0 -0
  207. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1217_claimed_representations.py +0 -0
  208. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1217_the_facility_is_shared.py +0 -0
  209. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1219_location_claims_its_own_axis.py +0 -0
  210. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1229_ledger_member_keying.py +0 -0
  211. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1240_iso_above_strip_visibility.py +0 -0
  212. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1254_turned_chamfers.py +0 -0
  213. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1260_view_constraints.py +0 -0
  214. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1276_turned_leader_targets.py +0 -0
  215. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1281_turned_fillets.py +0 -0
  216. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_1312_engine_costs.py +0 -0
  217. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_367_leader_ink.py +0 -0
  218. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_676_polygonal_boss.py +0 -0
  219. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_676_polygonal_boss_declare.py +0 -0
  220. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_740_leader_assignment.py +0 -0
  221. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_798_floor_cardinality.py +0 -0
  222. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_798_material_field.py +0 -0
  223. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_798_silhouette_lint.py +0 -0
  224. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_885_prismatic_coverage.py +0 -0
  225. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_909_sloped_profile.py +0 -0
  226. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_917_open_channel.py +0 -0
  227. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_924_zero_length_shoulders.py +0 -0
  228. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_955_height_contingency.py +0 -0
  229. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_issue_958_cross_solid_recognition.py +0 -0
  230. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_label_provenance.py +0 -0
  231. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_layout.py +0 -0
  232. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_layout_cleanliness.py +0 -0
  233. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_layout_hypothesis.py +0 -0
  234. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_layout_property.py +0 -0
  235. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_leader_footprint.py +0 -0
  236. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_lint_box_cache.py +0 -0
  237. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_lint_reconciliation.py +0 -0
  238. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_linting.py +0 -0
  239. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_location_vocabulary.py +0 -0
  240. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_note_verb.py +0 -0
  241. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_object_aspects.py +0 -0
  242. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_occupancy_boxes.py +0 -0
  243. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_parameter_id.py +0 -0
  244. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_part_model.py +0 -0
  245. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_pitch_dim_footprint.py +0 -0
  246. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_pmi.py +0 -0
  247. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_pmi_datum_lowering.py +0 -0
  248. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_pmi_gtol_lowering.py +0 -0
  249. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_pmi_part21.py +0 -0
  250. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_pocket_pattern.py +0 -0
  251. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_pocket_pattern_recognition.py +0 -0
  252. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_principal_profile_classifier.py +0 -0
  253. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_private_test_attr_reads.py +0 -0
  254. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_private_test_imports.py +0 -0
  255. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_quality_components.py +0 -0
  256. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_recogniser_adoption.py +0 -0
  257. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_recogniser_contract.py +0 -0
  258. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_recognition.py +0 -0
  259. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_recognition_manifest.py +0 -0
  260. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_recognition_result.py +0 -0
  261. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_refactor_golden.py +0 -0
  262. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_registry.py +0 -0
  263. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_render_seam.py +0 -0
  264. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_repack_geometry_seam.py +0 -0
  265. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_score.py +0 -0
  266. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_script_detail_parity.py +0 -0
  267. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_shared_box_memo.py +0 -0
  268. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_sheet_emit.py +0 -0
  269. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_sheet_gdt.py +0 -0
  270. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_sheet_identity_invariant.py +0 -0
  271. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_sheet_notes.py +0 -0
  272. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_sheet_of.py +0 -0
  273. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_sheet_section.py +0 -0
  274. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_sheet_tables.py +0 -0
  275. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_slanted_blind_step.py +0 -0
  276. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_slot_completeness.py +0 -0
  277. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_slot_pattern.py +0 -0
  278. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_slot_pattern_recognition.py +0 -0
  279. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_soft_deprecation.py +0 -0
  280. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_solve_trace.py +0 -0
  281. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_step_analysis_evaluation.py +0 -0
  282. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_suppression_ledger.py +0 -0
  283. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_suppression_marks.py +0 -0
  284. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_tolerances.py +0 -0
  285. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_turned_steps.py +0 -0
  286. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_view_plan.py +0 -0
  287. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_witness_label_reconciliation.py +0 -0
  288. {draftwright-0.4.13 → draftwright-0.4.14}/tests/test_workflows.py +0 -0
@@ -2,8 +2,27 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ### Added
6
+
7
+ - New lint code `annotation_ink_overlap`: reports another annotation's line-work —
8
+ a dimension line, an extension line or a leader shaft — drawn through a label's
9
+ text, which `annotation_overlap` cannot see because it compares label boxes to
10
+ each other and to nothing else. Measured as the longest connected stroke path
11
+ crossing the label's exact polygon (or its axis-aligned box), in millimetres,
12
+ with a 0.5 mm floor. It scores on legibility, so
13
+ dense sheets that previously scored clean now score lower — 17 findings on
14
+ NIST CTC-02 AP203 alone, every sampled one confirmed a real defect by rendering.
15
+ Repair does not yet act on it (#1333) and the corridor solve does not yet avoid
16
+ it (#1334) (#1321, #1332).
17
+
5
18
  ### Changed
6
19
 
20
+ - `annotation_overlap` now reports the whole defect when the same pair also draws
21
+ line-work through one of the two labels: the message names the crosser, the label
22
+ crossed and the millimetres, and replaces the "use `label_offset_x`" remedy, which
23
+ is the advice that case contradicts. `LintIssue.suggestion` is `None` for those
24
+ issues rather than a snippet that re-places the text (#1321, #1332).
25
+
7
26
  - Recogniser integration now uses only the exact `b123d-recognisers` dependency in
8
27
  `pyproject.toml` and `uv.lock`. The separate release-evidence record, dependency-bump workflow,
9
28
  candidate-checkout harness, and release-time capability-version rewrites have been removed;
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: draftwright
3
- Version: 0.4.13
3
+ Version: 0.4.14
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
@@ -685,7 +685,7 @@ Classifier: Topic :: Scientific/Engineering
685
685
  Requires-Python: <3.15,>=3.10
686
686
  Requires-Dist: antlr4-python3-runtime<4.10
687
687
  Requires-Dist: b123d-recognisers==0.3.1
688
- Requires-Dist: build123d-drafting-helpers>=0.14.1
688
+ Requires-Dist: build123d-drafting-helpers>=0.14.2
689
689
  Requires-Dist: build123d<0.11,>=0.9.0; python_full_version < '3.13'
690
690
  Requires-Dist: build123d<0.12,>=0.11; python_full_version >= '3.13'
691
691
  Requires-Dist: numpy>=1.24
@@ -64,3 +64,34 @@ pass.
64
64
  `bosses` is the fully consumed reference family. `repeating-radial-profiles` is the opposite
65
65
  reference: it remains geometry-only critique evidence for a separately authored gear declaration,
66
66
  with no inferred gear feature added to fill the table.
67
+
68
+ ## Passage compatibility boundary
69
+
70
+ The installed `b123d-recognisers==0.3.1` release already contains the `passages` family introduced
71
+ in 0.2.6. Draftwright declares its schema-v1 `Passage` output exhaustively but deliberately keeps
72
+ the family `unsupported`, with the drafting decision tracked by issue #1245. This is a truthful
73
+ consumer disposition: the geometry remains visible in the aggregate inventory, while Draftwright
74
+ does not invent an IR feature, DSL declaration, generated code, drawing annotation, or completeness
75
+ requirement for it.
76
+
77
+ Draftwright's exact dependency pin must remain on a reviewed released version in the interval
78
+ `>=0.2.6,<0.4.0` until the separately reviewed F4b transition is ready. The lower boundary ensures
79
+ the installed manifest really contains `passages`; the upper boundary prevents accidental adoption
80
+ of the planned 0.4 compatibility change.
81
+
82
+ The reviewed future 0.4 model is a distinct migration, not latent behavior in this declaration:
83
+
84
+ - `SectionPassage` will be the authoritative physical output and aggregate census source;
85
+ - legacy `Passage` values will be an accepted-only compatibility projection;
86
+ - `recognise_passages(..., ledger=...)` will be a fail-loud unavailable compatibility operation;
87
+ - the writer-free `recognise_passages` name will remain public but non-authoritative; and
88
+ - rich split-junction passages can supersede a currently rendered Slot claim, moving ownership to
89
+ the Passage family through `SLOT_SUPERSEDED_BY_PASSAGE`; and
90
+ - the capability contract must represent those API and per-output roles exhaustively before the
91
+ dependency pin can cross 0.4.
92
+
93
+ That migration must update the package release, Draftwright adapter and declaration model,
94
+ validator, tests, exact pin, and lockfile together. The current validator remains unchanged and
95
+ fail closed; there is no forward declaration or stale-manifest exception. Crossing 0.4 also
96
+ requires #1245 to decide what those newly Passage-owned occurrences draw, or an explicit reviewed
97
+ decision to accept the resulting loss of the existing Slot callout.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "draftwright"
7
- version = "0.4.13"
7
+ version = "0.4.14"
8
8
  description = "Automated technical-drawing generation for build123d"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -17,7 +17,7 @@ dependencies = [
17
17
  # 3.13+. Mirrors pzfreo/build123d-mcp's dependency handling.
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
- "build123d-drafting-helpers>=0.14.1",
20
+ "build123d-drafting-helpers>=0.14.2",
21
21
  "b123d-recognisers==0.3.1",
22
22
  "numpy>=1.24",
23
23
  # PNG raster export (SVG→PDF→PNG). Both permissively licensed (Pillow HPND; pypdfium2
@@ -358,7 +358,10 @@ _QUOTED_RE = re.compile(r"'([^']*)'")
358
358
 
359
359
  @functools.lru_cache(maxsize=512)
360
360
  def _text_size(
361
- text: str, font_size: float, font_path: str | None = PLEX_MONO, font: str = "Arial"
361
+ text: str,
362
+ font_size: float,
363
+ font_path: str | None = PLEX_MONO,
364
+ font: str = "Arial",
362
365
  ) -> tuple[float, float]:
363
366
  """Measured rendered (width, height) (page-mm) of *text* at *font_size*.
364
367
 
@@ -19,17 +19,21 @@ from typing import Any
19
19
  from build123d_drafting.helpers import DEFAULT_FONT_PATH, Dimension, Note, SafeDimension
20
20
 
21
21
  from draftwright._core import ( # noqa: F401 — _anno_box re-exported (#700)
22
+ _MARGIN,
22
23
  _anno_box,
23
24
  _decode_hole_location_fact,
25
+ _dim,
24
26
  _text_size,
25
27
  place_annotation,
26
28
  )
27
29
  from draftwright._geometry import ( # noqa: F401
28
30
  _boxes_overlap,
31
+ _segment_clip_extent,
29
32
  _segment_clips_box,
30
33
  _segment_crosses_box,
31
34
  )
32
35
  from draftwright.layout import StripCandidate, plan_strip
36
+ from draftwright.linting.ink_overlap import MIN_CROSSING_MM, crossable_region, crossing_length
33
37
  from draftwright.linting.issues import LintIssue
34
38
  from draftwright.linting.structural import (
35
39
  _ann_box,
@@ -40,6 +44,9 @@ from draftwright.model.compiled import resolve_feature
40
44
  _log = logging.getLogger(__name__)
41
45
 
42
46
 
47
+ _LABEL_INK_CLEARANCE_MM = 0.25
48
+
49
+
43
50
  def _with_hole_location_coverage(annotation, coverage):
44
51
  """Attach exact compiler location/member facts to a rendered annotation.
45
52
 
@@ -1458,6 +1465,321 @@ def box_within_page_and_clear(bb, page_box, obstacles) -> bool:
1458
1465
  )
1459
1466
 
1460
1467
 
1468
+ def prevent_dimension_label_ink(
1469
+ dimensions,
1470
+ *,
1471
+ page=None,
1472
+ immutable=(),
1473
+ obstacles=(),
1474
+ ):
1475
+ """Choose small along-line label offsets for a just-built dimension batch.
1476
+
1477
+ Corridor candidates used to reserve only their stacking tier while they were being
1478
+ solved. Their *eventual* decomposed ink became an obstacle after commit, but siblings
1479
+ built in the same batch could therefore put an extension line or an external-arrow tip
1480
+ through one another's labels (#1334). Immediate step-length chains have the identical
1481
+ gap: they build the whole row before any member is visible to strip occupancy.
1482
+
1483
+ This is the shared, bounded candidate-selection seam for both producers. It inspects
1484
+ the same public ``segments``/exact-label-region arithmetic as the lint backstop.
1485
+ A clean batch returns the same objects immediately. Only a conflicting batch explores
1486
+ a bounded set of analytically-derived label centres, rebuilding the selected survivors
1487
+ through their ``_dw_spec``. No full lint scan and no CAD boolean participates.
1488
+
1489
+ The label *centre* stays within half a millimetre of its measured span (rather than
1490
+ requiring the whole label to fit inside it). That distinction matters for short
1491
+ dimensions: their text is wider than the measured span by construction, yet a small
1492
+ shift can keep a shared witness out of the digits without making the label read as its
1493
+ neighbour's. If the bounded choices cannot improve the batch, the natural deterministic
1494
+ placement survives and the normal ``annotation_ink_overlap`` lint remains explicit
1495
+ evidence of the infeasible fallback. Names in *immutable* are never shifted (pins win).
1496
+ Fixed *obstacles* do not make an existing contact this local batch's responsibility, but
1497
+ no selected move may introduce a new label contact with one.
1498
+
1499
+ Returns ``[(name, dimension), ...]`` in input order.
1500
+ """
1501
+
1502
+ original = list(dimensions)
1503
+ if len(original) < 2:
1504
+ return original
1505
+ immutable = set(immutable)
1506
+ obstacles = tuple(obstacles)
1507
+
1508
+ def _axis_info(dim):
1509
+ spec = getattr(dim, "_dw_spec", None)
1510
+ label = getattr(dim, "label_bbox", None)
1511
+ if spec is None or label is None:
1512
+ return None
1513
+ dx = float(spec.p2[0]) - float(spec.p1[0])
1514
+ dy = float(spec.p2[1]) - float(spec.p1[1])
1515
+ if min(abs(dx), abs(dy)) > 0.1 or max(abs(dx), abs(dy)) <= 1e-9:
1516
+ return None # rotated labels need polygonal motion, not an AABB-axis solve
1517
+ axis = 1 if abs(dy) > abs(dx) else 0
1518
+ other = 1 - axis
1519
+ return axis, other, spec
1520
+
1521
+ infos = [_axis_info(dim) for _name, dim in original]
1522
+ if not any(info is not None for info in infos):
1523
+ return original
1524
+
1525
+ box_cache: dict[int, Any] = {}
1526
+ segments_cache: dict[int, Any] = {}
1527
+ tips_cache: dict[int, Any] = {}
1528
+
1529
+ def _box(dim):
1530
+ key = id(dim)
1531
+ if key in box_cache:
1532
+ return box_cache[key]
1533
+ raw = getattr(dim, "label_bbox", None)
1534
+ box_cache[key] = tuple(float(value) for value in raw) if raw is not None else None
1535
+ return box_cache[key]
1536
+
1537
+ def _segments(dim):
1538
+ key = id(dim)
1539
+ if key in segments_cache:
1540
+ return segments_cache[key]
1541
+ try:
1542
+ segments_cache[key] = tuple(
1543
+ (
1544
+ (float(first[0]), float(first[1])),
1545
+ (float(second[0]), float(second[1])),
1546
+ )
1547
+ for first, second in (getattr(dim, "segments", ()) or ())
1548
+ )
1549
+ except Exception: # noqa: BLE001 — optional metadata fails closed to lint
1550
+ segments_cache[key] = ()
1551
+ return segments_cache[key]
1552
+
1553
+ def _arrow_tips(dim, info):
1554
+ """Dimension terminator tips from its measured endpoints and dim-line ordinate.
1555
+
1556
+ Helpers expose line pieces but not the filled arrow triangles. Their tips remain
1557
+ exactly the two measured ordinates on the label's dimension line; treating the tip
1558
+ attachment as label-blocking catches the short-span arrow-through-digit case without
1559
+ inflating every shaft into a coarse full-geometry box.
1560
+ """
1561
+ key = id(dim)
1562
+ if key in tips_cache:
1563
+ return tips_cache[key]
1564
+ if info is None or (label := _box(dim)) is None:
1565
+ return ()
1566
+ axis, other, spec = info
1567
+ line = (label[other] + label[other + 2]) / 2.0
1568
+ result = []
1569
+ for point in (spec.p1, spec.p2):
1570
+ tip = [0.0, 0.0]
1571
+ tip[axis] = float(point[axis])
1572
+ tip[other] = line
1573
+ result.append(tuple(tip))
1574
+ tips_cache[key] = tuple(result)
1575
+ return tips_cache[key]
1576
+
1577
+ natural_obstacle_hits = [
1578
+ frozenset(
1579
+ obstacle_index
1580
+ for obstacle_index, obstacle in enumerate(obstacles)
1581
+ if label is not None and _boxes_overlap(label, obstacle)
1582
+ )
1583
+ for _name, dim in original
1584
+ for label in (_box(dim),)
1585
+ ]
1586
+
1587
+ def _conflicts(batch):
1588
+ """Stable conflict tokens; their count is the local solve's primary objective."""
1589
+ labels = [_box(dim) for _name, dim in batch]
1590
+ segs = [_segments(dim) for _name, dim in batch]
1591
+ regions = [
1592
+ crossable_region(label, item=dim, segments=segments)
1593
+ for (_name, dim), label, segments in zip(batch, labels, segs, strict=True)
1594
+ ]
1595
+ tips = [_arrow_tips(dim, info) for (_name, dim), info in zip(batch, infos, strict=True)]
1596
+ found: set[tuple] = set()
1597
+ for target, (label, region) in enumerate(zip(labels, regions, strict=True)):
1598
+ if label is None or region is None:
1599
+ continue
1600
+ # Helpers expose no arrow polygons. The foreign dimension's exact
1601
+ # attachment tips still participate, closing the line-metadata gap without
1602
+ # guessing the orientation of this dimension's own inside/outside arrows.
1603
+ for source, source_tips in enumerate(tips):
1604
+ if source == target:
1605
+ continue
1606
+ info = infos[target]
1607
+ if info is None:
1608
+ continue
1609
+ axis, other, _spec = info
1610
+ for tip_index, tip in enumerate(source_tips):
1611
+ if (
1612
+ label[axis] + _LABEL_INK_CLEARANCE_MM
1613
+ < tip[axis]
1614
+ < label[axis + 2] - _LABEL_INK_CLEARANCE_MM
1615
+ and label[other] - 1e-6 < tip[other] < label[other + 2] + 1e-6
1616
+ ):
1617
+ found.add(("arrow", source, tip_index, target))
1618
+ for source, source_segments in enumerate(segs):
1619
+ if source == target:
1620
+ continue # the helper deliberately cuts its own line around its label
1621
+ if crossing_length(source_segments, region) >= MIN_CROSSING_MM:
1622
+ found.add(("line", source, target))
1623
+ for obstacle_index, obstacle in enumerate(obstacles):
1624
+ if obstacle_index not in natural_obstacle_hits[target] and _boxes_overlap(
1625
+ label, obstacle
1626
+ ):
1627
+ found.add(("fixed", obstacle_index, target))
1628
+ for left, left_box in enumerate(labels):
1629
+ if left_box is None:
1630
+ continue
1631
+ for right in range(left + 1, len(labels)):
1632
+ right_box = labels[right]
1633
+ if right_box is not None and _boxes_overlap(left_box, right_box):
1634
+ found.add(("label", left, right))
1635
+ return frozenset(found)
1636
+
1637
+ natural_centres = []
1638
+ for (_name, dim), info in zip(original, infos, strict=True):
1639
+ label = _box(dim)
1640
+ natural_centres.append(
1641
+ None if info is None or label is None else (label[info[0]] + label[info[0] + 2]) / 2.0
1642
+ )
1643
+
1644
+ def _objective(batch):
1645
+ conflicts = _conflicts(batch)
1646
+ fixed_conflicts = sum(conflict[0] == "fixed" for conflict in conflicts)
1647
+ offsets = []
1648
+ for (_name, dim), info, natural in zip(batch, infos, natural_centres, strict=True):
1649
+ label = _box(dim)
1650
+ offsets.append(
1651
+ 0.0
1652
+ if info is None or natural is None or label is None
1653
+ else abs((label[info[0]] + label[info[0] + 2]) / 2.0 - natural)
1654
+ )
1655
+ return (
1656
+ fixed_conflicts,
1657
+ len(conflicts),
1658
+ round(sum(offsets), 9),
1659
+ round(max(offsets, default=0.0), 9),
1660
+ tuple(round(value, 9) for value in offsets),
1661
+ ), conflicts
1662
+
1663
+ current = list(original)
1664
+ current_objective, conflicts = _objective(current)
1665
+ if not conflicts:
1666
+ return current # overwhelmingly common path: no extra Dimension construction
1667
+
1668
+ cache: dict[tuple[int, float], Any] = {}
1669
+
1670
+ def _rebuild(index, centre):
1671
+ key = (index, round(centre, 6))
1672
+ if key in cache:
1673
+ return cache[key]
1674
+ name, dim = original[index]
1675
+ info = infos[index]
1676
+ natural = natural_centres[index]
1677
+ if info is None or natural is None:
1678
+ return dim
1679
+ axis, _other, spec = info
1680
+ direction = 1.0 if float(spec.p2[axis]) >= float(spec.p1[axis]) else -1.0
1681
+ kwargs = dict(spec.kwargs)
1682
+ kwargs["label_offset_x"] = (
1683
+ kwargs.get("label_offset_x", 0.0) + (centre - natural) * direction
1684
+ )
1685
+ rebuilt = _dim(spec.p1, spec.p2, spec.side, spec.distance, spec.draft, **kwargs)
1686
+ # The producer attaches semantic evidence before the corridor commits the
1687
+ # survivor. Rebuilding only the rendered Dimension must not erase that evidence:
1688
+ # hole-table escalation and coverage lint read these riders from the final object.
1689
+ # Copy semantic namespaces, never helper geometry internals; `_dw_spec` belongs to
1690
+ # the rebuilt placement and must remain the one `_dim` just created.
1691
+ for attr, value in vars(dim).items():
1692
+ if attr.startswith("covers_") or (attr.startswith("_dw_") and attr != "_dw_spec"):
1693
+ setattr(rebuilt, attr, value)
1694
+ cache[key] = rebuilt
1695
+ return rebuilt
1696
+
1697
+ def _centres_for(index, batch):
1698
+ info = infos[index]
1699
+ target = batch[index][1]
1700
+ label = _box(target)
1701
+ if info is None or label is None:
1702
+ return ()
1703
+ axis, other, spec = info
1704
+ current_centre = (label[axis] + label[axis + 2]) / 2.0
1705
+ half = (label[axis + 2] - label[axis]) / 2.0
1706
+ # The centre remains attached to its own measured span. A half-millimetre
1707
+ # overhang admits the minimum clear position for text wider than a short span.
1708
+ lo, hi = sorted((float(spec.p1[axis]), float(spec.p2[axis])))
1709
+ lo -= MIN_CROSSING_MM
1710
+ hi += MIN_CROSSING_MM
1711
+ if page is not None:
1712
+ lo = max(lo, float(page[axis]) + half)
1713
+ hi = min(hi, float(page[axis + 2]) - half)
1714
+ if lo > hi:
1715
+ return ()
1716
+ choices = {min(max(current_centre, lo), hi), lo, hi}
1717
+ for source, ((_name, dim), source_info) in enumerate(zip(batch, infos, strict=True)):
1718
+ source_label = _box(dim)
1719
+ if source != index and source_label is not None:
1720
+ # Existing labels are obstacles too; moving out of line-work must not
1721
+ # exchange one illegibility defect for text-on-text.
1722
+ if (
1723
+ source_label[other] < label[other + 2]
1724
+ and source_label[other + 2] > label[other]
1725
+ ):
1726
+ choices.add(source_label[axis] - half - _LABEL_INK_CLEARANCE_MM)
1727
+ choices.add(source_label[axis + 2] + half + _LABEL_INK_CLEARANCE_MM)
1728
+ if source == index:
1729
+ continue
1730
+ for tip in _arrow_tips(dim, source_info):
1731
+ if label[other] - 1e-6 <= tip[other] <= label[other + 2] + 1e-6:
1732
+ choices.add(tip[axis] - half - _LABEL_INK_CLEARANCE_MM)
1733
+ choices.add(tip[axis] + half + _LABEL_INK_CLEARANCE_MM)
1734
+ # Clip each foreign segment only to this label's fixed-axis band. The
1735
+ # resulting projection is the exact interval its centre must clear.
1736
+ band = [-1e9, -1e9, 1e9, 1e9]
1737
+ band[other], band[other + 2] = label[other], label[other + 2]
1738
+ for segment in _segments(dim):
1739
+ clipped = _segment_clip_extent(segment[0], segment[1], tuple(band), pad=0.0)
1740
+ if clipped is None:
1741
+ continue
1742
+ choices.add(clipped[axis] - half - _LABEL_INK_CLEARANCE_MM)
1743
+ choices.add(clipped[axis + 2] + half + _LABEL_INK_CLEARANCE_MM)
1744
+ bounded = {min(max(value, lo), hi) for value in choices if math.isfinite(value)}
1745
+ # Bound exceptional work. Nearest candidates are the meaningful drafting
1746
+ # alternatives; the two association bounds remain present explicitly.
1747
+ nearest = sorted(bounded, key=lambda value: (abs(value - current_centre), value))[:16]
1748
+ return tuple(dict.fromkeys([*nearest, lo, hi]))
1749
+
1750
+ # Deterministic steepest descent. Every accepted move strictly improves the
1751
+ # lexicographic objective, so it terminates even when the layout is infeasible.
1752
+ for _iteration in range(max(1, 2 * len(current))):
1753
+ involved: set[int] = set()
1754
+ for conflict in conflicts:
1755
+ if conflict[0] == "arrow":
1756
+ involved.update((conflict[1], conflict[3])) # source + crossed label
1757
+ elif conflict[0] == "line":
1758
+ involved.update((conflict[1], conflict[2])) # source + crossed label
1759
+ elif conflict[0] == "fixed":
1760
+ involved.add(conflict[2])
1761
+ else: # label/label
1762
+ involved.update((conflict[1], conflict[2]))
1763
+ best = None
1764
+ for index in sorted(involved):
1765
+ name, _dim_obj = original[index]
1766
+ if name in immutable or infos[index] is None:
1767
+ continue
1768
+ for centre in _centres_for(index, current):
1769
+ trial = list(current)
1770
+ trial[index] = (name, _rebuild(index, centre))
1771
+ objective, trial_conflicts = _objective(trial)
1772
+ key = (objective, index, round(centre, 9))
1773
+ if objective < current_objective and (best is None or key < best[0]):
1774
+ best = (key, trial, objective, trial_conflicts)
1775
+ if best is None:
1776
+ break
1777
+ _key, current, current_objective, conflicts = best
1778
+ if not conflicts:
1779
+ break
1780
+ return current
1781
+
1782
+
1461
1783
  # ── The corridor priority ladder (#894) ───────────────────────────────────────
1462
1784
  # `CorridorCandidate.priority` is an over-capacity survival rank, and a rank only
1463
1785
  # means anything RELATIVE to the other rungs. Defining the whole ladder here — beside
@@ -2126,6 +2448,19 @@ def place_strip_candidates(
2126
2448
  tp["free_segments"] = [list(s) for s in segs]
2127
2449
  todo = list(cands)
2128
2450
 
2451
+ def _real_box_conflict(name, real):
2452
+ """Return the hard-obstacle reason for a built survivor, if any."""
2453
+ if real is None:
2454
+ return None
2455
+ if not force and _box_hits(real, blockers):
2456
+ return "real_box_corridor_blocked"
2457
+ fb = (forbid or {}).get(name)
2458
+ if fb is not None and _box_hits(real, (fb,)):
2459
+ return "real_box_forbid"
2460
+ if _box_hits(real, out_of_band):
2461
+ return "real_box_out_of_band"
2462
+ return None
2463
+
2129
2464
  def _take_for_segment(items, n):
2130
2465
  if len(items) <= n:
2131
2466
  return items, []
@@ -2210,6 +2545,7 @@ def place_strip_candidates(
2210
2545
  accepted.append(((name, build), pos))
2211
2546
  return accepted, rejected
2212
2547
 
2548
+ solved = []
2213
2549
  for seg_lo, seg_hi in segs:
2214
2550
  if not todo:
2215
2551
  break
@@ -2231,43 +2567,60 @@ def place_strip_candidates(
2231
2567
  for (name, build), pos in accepted:
2232
2568
  dim = build(pos)
2233
2569
  real = _geom_box(dim)
2234
- if real is not None:
2235
- if not force and _box_hits(real, blockers):
2236
- if tp is not None:
2237
- tp["rejected"].append(
2238
- {"name": name, "reason": "real_box_corridor_blocked"}
2239
- )
2240
- rejected_total.append((name, build))
2241
- continue
2242
- fb = (forbid or {}).get(name)
2243
- if fb is not None and _box_hits(real, (fb,)):
2244
- if tp is not None:
2245
- tp["rejected"].append({"name": name, "reason": "real_box_forbid"})
2246
- rejected_total.append((name, build))
2247
- continue
2248
- # A survivor whose real geometry escaped the batch's predicted
2249
- # perpendicular band could overlap an obstacle the carve was never
2250
- # shown (review #679) — the one collision class the stacking-axis
2251
- # model cannot tolerate. Never fires while predictions are accurate.
2252
- if _box_hits(real, out_of_band):
2253
- if tp is not None:
2254
- tp["rejected"].append({"name": name, "reason": "real_box_out_of_band"})
2255
- rejected_total.append((name, build))
2256
- continue
2570
+ if reason := _real_box_conflict(name, real):
2571
+ if tp is not None:
2572
+ tp["rejected"].append({"name": name, "reason": reason})
2573
+ rejected_total.append((name, build))
2574
+ continue
2257
2575
  placed.append((name, dim))
2258
2576
  if tp is not None:
2259
2577
  tp["placed"].append({"name": name, "pos": pos})
2260
2578
  todo = todo + rejected_total
2261
- for name, dim in placed:
2262
- # Record feature provenance (ADR 0010): the drain-time seam for corridor-placed
2263
- # dims — `features` maps this batch's names to their source IR feature.
2264
- ctx.place(
2265
- dim,
2266
- name,
2267
- view=view,
2268
- feature=(features or {}).get(name),
2269
- measurement=(measurements or {}).get(name),
2270
- )
2579
+ solved.extend(placed)
2580
+
2581
+ if len(solved) > 1:
2582
+ assert len({name for name, _dim_obj in solved}) == len(solved), (
2583
+ "strip survivor names must be unique before label selection"
2584
+ )
2585
+ natural_solved = dict(solved)
2586
+ adjusted = prevent_dimension_label_ink(
2587
+ solved,
2588
+ page=(
2589
+ (
2590
+ _MARGIN,
2591
+ _MARGIN,
2592
+ float(dwg.page_w) - _MARGIN,
2593
+ float(dwg.page_h) - _MARGIN,
2594
+ )
2595
+ if hasattr(dwg, "page_w") and hasattr(dwg, "page_h")
2596
+ else None
2597
+ ),
2598
+ immutable={name for name, _dim_obj in solved if (anchored or {}).get(name, False)},
2599
+ )
2600
+ # A label shift normally stays inside the dimension's measured span and
2601
+ # therefore inside its already-validated footprint. Keep the validation
2602
+ # contract explicit nevertheless: if an unusual helper/style grows the real
2603
+ # box into a hard obstacle, retain the natural survivor. The lint backstop then
2604
+ # records the infeasible ink conflict instead of this prevention step trading
2605
+ # it for a structural collision.
2606
+ solved = []
2607
+ for name, dim in adjusted:
2608
+ natural = natural_solved[name]
2609
+ if dim is natural:
2610
+ solved.append((name, dim))
2611
+ continue
2612
+ real = _geom_box(dim)
2613
+ solved.append((name, natural if _real_box_conflict(name, real) else dim))
2614
+ for name, dim in solved:
2615
+ # Record feature provenance (ADR 0010): the drain-time seam for corridor-placed
2616
+ # dims — `features` maps this batch's names to their source IR feature.
2617
+ ctx.place(
2618
+ dim,
2619
+ name,
2620
+ view=view,
2621
+ feature=(features or {}).get(name),
2622
+ measurement=(measurements or {}).get(name),
2623
+ )
2271
2624
  if tp is not None:
2272
2625
  tp["unplaced"] = [n for n, _ in todo]
2273
2626
  trace.end_pass(tp) # folds a standalone pass's items; no-op when corridor-nested
@@ -81,6 +81,7 @@ from draftwright.annotations._common import (
81
81
  full_strip_message,
82
82
  leader_callout_geometry,
83
83
  place_strip_candidates,
84
+ prevent_dimension_label_ink,
84
85
  register_corridor,
85
86
  strip_free_span,
86
87
  strip_obstacles,
@@ -3686,8 +3687,22 @@ def _draw_step_chain(
3686
3687
  )
3687
3688
  )
3688
3689
 
3689
- # Room guard: if any dim would fall off the drawable page, place NONE.
3690
3690
  page = (_MARGIN, _MARGIN, dwg.page_w - _MARGIN, dwg.page_h - _MARGIN)
3691
+ # The chain is one placement batch: until commit, no sibling's extension line or
3692
+ # terminator exists in strip occupancy. Select small along-line label offsets against
3693
+ # the complete batch before the room guard (#1334). Measurement provenance stays paired
3694
+ # by name; only the rendered Dimension survivor changes.
3695
+ measurements_by_name = {name: measurements for name, _dim_obj, measurements in candidates}
3696
+ candidates = [
3697
+ (name, dim, measurements_by_name[name])
3698
+ for name, dim in prevent_dimension_label_ink(
3699
+ [(name, dim) for name, dim, _measurements in candidates],
3700
+ page=page,
3701
+ obstacles=strip_obstacles(dwg, view=view, crossable=CROSSABLE_TYPES),
3702
+ )
3703
+ ]
3704
+
3705
+ # Room guard: if any dim would fall off the drawable page, place NONE.
3691
3706
  for _, dim, _measurements in candidates:
3692
3707
  box = _anno_box(dim)
3693
3708
  if box is not None and not (