draftwright 0.4.13__tar.gz → 0.4.15__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 (289) hide show
  1. {draftwright-0.4.13 → draftwright-0.4.15}/CHANGELOG.md +19 -0
  2. {draftwright-0.4.13 → draftwright-0.4.15}/PKG-INFO +2 -2
  3. {draftwright-0.4.13 → draftwright-0.4.15}/docs/reference/recogniser-capabilities.md +31 -0
  4. {draftwright-0.4.13 → draftwright-0.4.15}/pyproject.toml +2 -2
  5. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/_core.py +4 -1
  6. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/annotations/_common.py +424 -33
  7. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/annotations/from_model.py +41 -15
  8. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/builder.py +467 -89
  9. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/compose.py +9 -4
  10. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/drawing.py +13 -1
  11. draftwright-0.4.15/src/draftwright/linting/ink_overlap.py +745 -0
  12. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/linting/quality.py +1 -0
  13. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/linting/structural.py +161 -13
  14. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/linting/suggest.py +7 -0
  15. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/repair.py +26 -0
  16. {draftwright-0.4.13 → draftwright-0.4.15}/tests/conftest.py +2 -0
  17. draftwright-0.4.15/tests/fixtures/grm03_thumbwheel_drive_screw_ap242_pmi.step +32991 -0
  18. {draftwright-0.4.13 → draftwright-0.4.15}/tests/refactor_golden/flange_dense.json +70 -89
  19. {draftwright-0.4.13 → draftwright-0.4.15}/tests/refactor_golden/turned_stepped.json +7 -26
  20. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_add_dimension.py +2 -1
  21. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_adr0018_view_selection.py +3 -4
  22. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_audit_differential.py +4 -1
  23. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_compiled_plan_boundary.py +2 -1
  24. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_e2e_standards.py +6 -3
  25. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1130_view_planning_evidence.py +8 -9
  26. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1166_cross_pass_feature_leaders.py +7 -1
  27. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1217_the_facility_is_shared.py +16 -3
  28. draftwright-0.4.15/tests/test_issue_1262_automatic_turned_views.py +85 -0
  29. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1296_cylindrical_diameter_pmi.py +1 -2
  30. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1298_manufacturing_requirements.py +4 -4
  31. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1299_page_escalation.py +46 -9
  32. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1308_machined_leader_analytics.py +15 -2
  33. draftwright-0.4.15/tests/test_issue_1332_overlap_remedy.py +89 -0
  34. draftwright-0.4.15/tests/test_issue_1332_overlap_remedy_fast.py +106 -0
  35. draftwright-0.4.15/tests/test_issue_1332_zone_labels.py +70 -0
  36. draftwright-0.4.15/tests/test_issue_1334_prevent_ink.py +187 -0
  37. draftwright-0.4.15/tests/test_issue_1336_grm03_ap242_pmi_fixture.py +77 -0
  38. draftwright-0.4.15/tests/test_issue_1338_scale_before_page_escalation.py +103 -0
  39. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_443_grm03_axial_coverage.py +10 -5
  40. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_798_silhouette_lint.py +8 -2
  41. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_915_dense_case.py +62 -2
  42. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_916_pocket_leader.py +29 -1
  43. draftwright-0.4.15/tests/test_lint_ink_overlap.py +727 -0
  44. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_lint_structural.py +56 -0
  45. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_make_drawing.py +139 -8
  46. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_recogniser_capabilities.py +40 -0
  47. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_script_detail_parity.py +2 -1
  48. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_strip_layout.py +10 -2
  49. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_view_plan.py +8 -2
  50. {draftwright-0.4.13 → draftwright-0.4.15}/.gitignore +0 -0
  51. {draftwright-0.4.13 → draftwright-0.4.15}/LICENSE +0 -0
  52. {draftwright-0.4.13 → draftwright-0.4.15}/README.md +0 -0
  53. {draftwright-0.4.13 → draftwright-0.4.15}/docs/adr/README.md +0 -0
  54. {draftwright-0.4.13 → draftwright-0.4.15}/docs/reference/sheet.md +0 -0
  55. {draftwright-0.4.13 → draftwright-0.4.15}/docs/research/1062-repeating-radial-profile-evidence.md +0 -0
  56. {draftwright-0.4.13 → draftwright-0.4.15}/skills/SKILL.md +0 -0
  57. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/__init__.py +0 -0
  58. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/_build_profile.py +0 -0
  59. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/_geometry.py +0 -0
  60. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/_pmi_part21.py +0 -0
  61. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/_warnings.py +0 -0
  62. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/analysis.py +0 -0
  63. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/annotate.py +0 -0
  64. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/annotations/__init__.py +0 -0
  65. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/annotations/balloons.py +0 -0
  66. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/annotations/gears.py +0 -0
  67. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/annotations/holes.py +0 -0
  68. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/annotations/leaders.py +0 -0
  69. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/annotations/orchestrator.py +0 -0
  70. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/annotations/sections.py +0 -0
  71. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/audit.py +0 -0
  72. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/cli.py +0 -0
  73. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/evaluation/__init__.py +0 -0
  74. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/evaluation/step_analysis.py +0 -0
  75. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/export.py +0 -0
  76. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/fits.py +0 -0
  77. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
  78. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
  79. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
  80. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
  81. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/fonts/__init__.py +0 -0
  82. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/intents.py +0 -0
  83. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/layout.py +0 -0
  84. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/linting/__init__.py +0 -0
  85. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/linting/channel_coverage.py +0 -0
  86. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/linting/coverage.py +0 -0
  87. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/linting/evidence.py +0 -0
  88. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/linting/flat_coverage.py +0 -0
  89. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/linting/gear_coverage.py +0 -0
  90. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/linting/hole_coverage.py +0 -0
  91. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/linting/issues.py +0 -0
  92. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/linting/pmi_coverage.py +0 -0
  93. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/linting/polygonal_stock_coverage.py +0 -0
  94. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/linting/profiled_bore_coverage.py +0 -0
  95. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/linting/slot_coverage.py +0 -0
  96. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/make_drawing.py +0 -0
  97. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/model/__init__.py +0 -0
  98. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/model/callout.py +0 -0
  99. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/model/compiled.py +0 -0
  100. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/model/declare.py +0 -0
  101. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/model/detect.py +0 -0
  102. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/model/ir.py +0 -0
  103. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/model/planner.py +0 -0
  104. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/model/pmi_lowering.py +0 -0
  105. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/pmi.py +0 -0
  106. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/projection.py +0 -0
  107. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/py.typed +0 -0
  108. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/recogniser_contract.py +0 -0
  109. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/recognition/__init__.py +0 -0
  110. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/recognition_cache.py +0 -0
  111. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/registry.py +0 -0
  112. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/score.py +0 -0
  113. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/sheet.py +0 -0
  114. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/sheet_emit.py +0 -0
  115. {draftwright-0.4.13 → draftwright-0.4.15}/src/draftwright/view_plan.py +0 -0
  116. {draftwright-0.4.13 → draftwright-0.4.15}/tests/_kernel.py +0 -0
  117. {draftwright-0.4.13 → draftwright-0.4.15}/tests/_layout_sig.py +0 -0
  118. {draftwright-0.4.13 → draftwright-0.4.15}/tests/fixtures/ap242_single_cylinder_diameter.step +0 -0
  119. {draftwright-0.4.13 → draftwright-0.4.15}/tests/fixtures/evaluation/README.md +0 -0
  120. {draftwright-0.4.13 → draftwright-0.4.15}/tests/fixtures/evaluation/ambiguous-open-semicircle.step +0 -0
  121. {draftwright-0.4.13 → draftwright-0.4.15}/tests/fixtures/evaluation/blind-hole.step +0 -0
  122. {draftwright-0.4.13 → draftwright-0.4.15}/tests/fixtures/evaluation/corpus-v1.json +0 -0
  123. {draftwright-0.4.13 → draftwright-0.4.15}/tests/fixtures/evaluation/plain-block.step +0 -0
  124. {draftwright-0.4.13 → draftwright-0.4.15}/tests/fixtures/evaluation/topology-a.step +0 -0
  125. {draftwright-0.4.13 → draftwright-0.4.15}/tests/fixtures/evaluation/topology-b.step +0 -0
  126. {draftwright-0.4.13 → draftwright-0.4.15}/tests/fixtures/grm03_thumbwheel_drive_screw.step +0 -0
  127. {draftwright-0.4.13 → draftwright-0.4.15}/tests/fixtures/grm04_drive_plate.step +0 -0
  128. {draftwright-0.4.13 → draftwright-0.4.15}/tests/fixtures/if_step_flat_across_cylinder.step +0 -0
  129. {draftwright-0.4.13 → draftwright-0.4.15}/tests/fixtures/issue_1058_wheel_rh.step +0 -0
  130. {draftwright-0.4.13 → draftwright-0.4.15}/tests/fixtures/issue_909_basic_part_design_017_body.step +0 -0
  131. {draftwright-0.4.13 → draftwright-0.4.15}/tests/fixtures/issue_915_case_study_2.step +0 -0
  132. {draftwright-0.4.13 → draftwright-0.4.15}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
  133. {draftwright-0.4.13 → draftwright-0.4.15}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
  134. {draftwright-0.4.13 → draftwright-0.4.15}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
  135. {draftwright-0.4.13 → draftwright-0.4.15}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
  136. {draftwright-0.4.13 → draftwright-0.4.15}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
  137. {draftwright-0.4.13 → draftwright-0.4.15}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
  138. {draftwright-0.4.13 → draftwright-0.4.15}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
  139. {draftwright-0.4.13 → draftwright-0.4.15}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
  140. {draftwright-0.4.13 → draftwright-0.4.15}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
  141. {draftwright-0.4.13 → draftwright-0.4.15}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
  142. {draftwright-0.4.13 → draftwright-0.4.15}/tests/fixtures/tuner_jig_blind_obround_pockets.step +0 -0
  143. {draftwright-0.4.13 → draftwright-0.4.15}/tests/refactor_golden/bracket_section.json +0 -0
  144. {draftwright-0.4.13 → draftwright-0.4.15}/tests/refactor_golden/centered_rebate.json +0 -0
  145. {draftwright-0.4.13 → draftwright-0.4.15}/tests/refactor_golden/chamfered.json +0 -0
  146. {draftwright-0.4.13 → draftwright-0.4.15}/tests/refactor_golden/filleted.json +0 -0
  147. {draftwright-0.4.13 → draftwright-0.4.15}/tests/refactor_golden/grid_plate.json +0 -0
  148. {draftwright-0.4.13 → draftwright-0.4.15}/tests/refactor_golden/grooved_shaft.json +0 -0
  149. {draftwright-0.4.13 → draftwright-0.4.15}/tests/refactor_golden/hex_bar.json +0 -0
  150. {draftwright-0.4.13 → draftwright-0.4.15}/tests/refactor_golden/holed_slot.json +0 -0
  151. {draftwright-0.4.13 → draftwright-0.4.15}/tests/refactor_golden/pocketed.json +0 -0
  152. {draftwright-0.4.13 → draftwright-0.4.15}/tests/refactor_golden/prismatic_ladder.json +0 -0
  153. {draftwright-0.4.13 → draftwright-0.4.15}/tests/refactor_golden/scattered_plate.json +0 -0
  154. {draftwright-0.4.13 → draftwright-0.4.15}/tests/refactor_golden/side_drilled.json +0 -0
  155. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_adr0018_arrangement_gate.py +0 -0
  156. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_adr0018_view_routing.py +0 -0
  157. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_agents_guide.py +0 -0
  158. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_api_docs.py +0 -0
  159. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_architecture_docs.py +0 -0
  160. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_balloon_ring_standoff.py +0 -0
  161. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_build_profile_harness.py +0 -0
  162. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_carve_free_position_callers.py +0 -0
  163. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_counting_calls.py +0 -0
  164. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_declare.py +0 -0
  165. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_declared_recognition_gate.py +0 -0
  166. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_dense_sheet_canary.py +0 -0
  167. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_deprecation_dates.py +0 -0
  168. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_detect_once.py +0 -0
  169. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_detect_registry.py +0 -0
  170. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_dimension_role_vocabulary.py +0 -0
  171. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_drawing_encapsulation.py +0 -0
  172. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_e2e_slice.py +0 -0
  173. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_export_dxf_curves.py +0 -0
  174. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_export_dxf_zoom.py +0 -0
  175. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_external_recognition_boundary.py +0 -0
  176. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_fillets_adjacency.py +0 -0
  177. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_fits.py +0 -0
  178. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_flat_completeness.py +0 -0
  179. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_flat_stock_identity.py +0 -0
  180. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_flat_stock_opposition.py +0 -0
  181. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_gdt_placement.py +0 -0
  182. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_grid_lattice_convention.py +0 -0
  183. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_import_boundaries.py +0 -0
  184. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_iso_nts_caption_placement.py +0 -0
  185. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1000_envelope_reuse.py +0 -0
  186. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1058_wheel_profile.py +0 -0
  187. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1073_cross_body_patterns.py +0 -0
  188. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1082_polygonal_stock.py +0 -0
  189. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1086_declared_gears.py +0 -0
  190. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1116_ap242_hole_tolerance_lowering.py +0 -0
  191. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1142_hole_leader_labels.py +0 -0
  192. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1143_hole_completeness.py +0 -0
  193. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1144_transactional_hole_table.py +0 -0
  194. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1146_scale_completeness.py +0 -0
  195. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1147_legibility_pair_aggregation.py +0 -0
  196. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1153_contradictory_dimensions.py +0 -0
  197. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1154_one_owner_per_measurement.py +0 -0
  198. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1155_grm04_sheet_use.py +0 -0
  199. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1176_quality_fidelity.py +0 -0
  200. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1177_angular_dimensions.py +0 -0
  201. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1187_unroutable_leaders.py +0 -0
  202. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1188_per_view_assignment.py +0 -0
  203. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1190_section_decision.py +0 -0
  204. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1196_deterministic_view_names.py +0 -0
  205. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1202_observed_drawing_consumer.py +0 -0
  206. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1204_multiscale_view_issues.py +0 -0
  207. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1209_linear_pmi_witnesses.py +0 -0
  208. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1215_envelope_tolerance.py +0 -0
  209. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1215_no_approved_tolerance_is_dropped.py +0 -0
  210. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1216_callout_reservation_measures_ink.py +0 -0
  211. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1216_pairwise_across_scale_groups.py +0 -0
  212. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1217_claimed_representations.py +0 -0
  213. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1219_location_claims_its_own_axis.py +0 -0
  214. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1229_ledger_member_keying.py +0 -0
  215. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1240_iso_above_strip_visibility.py +0 -0
  216. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1254_turned_chamfers.py +0 -0
  217. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1260_view_constraints.py +0 -0
  218. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1276_turned_leader_targets.py +0 -0
  219. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1281_turned_fillets.py +0 -0
  220. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_1312_engine_costs.py +0 -0
  221. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_367_leader_ink.py +0 -0
  222. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_676_polygonal_boss.py +0 -0
  223. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_676_polygonal_boss_declare.py +0 -0
  224. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_740_leader_assignment.py +0 -0
  225. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_798_floor_cardinality.py +0 -0
  226. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_798_material_field.py +0 -0
  227. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_885_prismatic_coverage.py +0 -0
  228. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_909_sloped_profile.py +0 -0
  229. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_917_open_channel.py +0 -0
  230. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_924_zero_length_shoulders.py +0 -0
  231. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_955_height_contingency.py +0 -0
  232. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_issue_958_cross_solid_recognition.py +0 -0
  233. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_label_provenance.py +0 -0
  234. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_layout.py +0 -0
  235. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_layout_cleanliness.py +0 -0
  236. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_layout_hypothesis.py +0 -0
  237. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_layout_property.py +0 -0
  238. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_leader_footprint.py +0 -0
  239. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_lint_box_cache.py +0 -0
  240. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_lint_reconciliation.py +0 -0
  241. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_linting.py +0 -0
  242. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_location_vocabulary.py +0 -0
  243. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_note_verb.py +0 -0
  244. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_object_aspects.py +0 -0
  245. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_occupancy_boxes.py +0 -0
  246. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_parameter_id.py +0 -0
  247. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_part_model.py +0 -0
  248. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_pitch_dim_footprint.py +0 -0
  249. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_pmi.py +0 -0
  250. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_pmi_datum_lowering.py +0 -0
  251. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_pmi_gtol_lowering.py +0 -0
  252. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_pmi_part21.py +0 -0
  253. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_pocket_pattern.py +0 -0
  254. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_pocket_pattern_recognition.py +0 -0
  255. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_principal_profile_classifier.py +0 -0
  256. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_private_test_attr_reads.py +0 -0
  257. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_private_test_imports.py +0 -0
  258. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_quality_components.py +0 -0
  259. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_recogniser_adoption.py +0 -0
  260. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_recogniser_contract.py +0 -0
  261. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_recognition.py +0 -0
  262. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_recognition_manifest.py +0 -0
  263. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_recognition_result.py +0 -0
  264. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_refactor_golden.py +0 -0
  265. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_registry.py +0 -0
  266. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_render_seam.py +0 -0
  267. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_repack_geometry_seam.py +0 -0
  268. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_score.py +0 -0
  269. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_shared_box_memo.py +0 -0
  270. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_sheet_emit.py +0 -0
  271. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_sheet_gdt.py +0 -0
  272. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_sheet_identity_invariant.py +0 -0
  273. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_sheet_notes.py +0 -0
  274. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_sheet_of.py +0 -0
  275. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_sheet_section.py +0 -0
  276. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_sheet_tables.py +0 -0
  277. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_slanted_blind_step.py +0 -0
  278. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_slot_completeness.py +0 -0
  279. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_slot_pattern.py +0 -0
  280. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_slot_pattern_recognition.py +0 -0
  281. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_soft_deprecation.py +0 -0
  282. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_solve_trace.py +0 -0
  283. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_step_analysis_evaluation.py +0 -0
  284. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_suppression_ledger.py +0 -0
  285. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_suppression_marks.py +0 -0
  286. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_tolerances.py +0 -0
  287. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_turned_steps.py +0 -0
  288. {draftwright-0.4.13 → draftwright-0.4.15}/tests/test_witness_label_reconciliation.py +0 -0
  289. {draftwright-0.4.13 → draftwright-0.4.15}/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.15
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.15"
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,359 @@ 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
+ perpendicular_step=None,
1475
+ ):
1476
+ """Choose small along-line label offsets for a just-built dimension batch.
1477
+
1478
+ Corridor candidates used to reserve only their stacking tier while they were being
1479
+ solved. Their *eventual* decomposed ink became an obstacle after commit, but siblings
1480
+ built in the same batch could therefore put an extension line or an external-arrow tip
1481
+ through one another's labels (#1334). Immediate step-length chains have the identical
1482
+ gap: they build the whole row before any member is visible to strip occupancy.
1483
+
1484
+ This is the shared, bounded candidate-selection seam for both producers. It inspects
1485
+ the same public ``segments``/exact-label-region arithmetic as the lint backstop.
1486
+ A clean batch returns the same objects immediately. Only a conflicting batch explores
1487
+ a bounded set of analytically-derived label centres, rebuilding the selected survivors
1488
+ through their ``_dw_spec``. No full lint scan and no CAD boolean participates.
1489
+
1490
+ The label *centre* stays within half a millimetre of its measured span (rather than
1491
+ requiring the whole label to fit inside it). That distinction matters for short
1492
+ dimensions: their text is wider than the measured span by construction, yet a small
1493
+ shift can keep a shared witness out of the digits without making the label read as its
1494
+ neighbour's. If the bounded choices cannot improve the batch, the natural deterministic
1495
+ placement survives and the normal ``annotation_ink_overlap`` lint remains explicit
1496
+ evidence of the infeasible fallback. Names in *immutable* are never shifted (pins win).
1497
+ When ``perpendicular_step`` is supplied, a conflicting dimension may also move one
1498
+ established stacking tier away from the view. This is the generic fallback for a chain
1499
+ whose line-work cannot be cleared by moving labels along their measured spans.
1500
+ Fixed *obstacles* do not make an existing contact this local batch's responsibility, but
1501
+ no selected move may introduce a new label contact with one.
1502
+
1503
+ Returns ``[(name, dimension), ...]`` in input order.
1504
+ """
1505
+
1506
+ original = list(dimensions)
1507
+ if len(original) < 2:
1508
+ return original
1509
+ immutable = set(immutable)
1510
+ obstacles = tuple(obstacles)
1511
+
1512
+ def _axis_info(dim):
1513
+ spec = getattr(dim, "_dw_spec", None)
1514
+ label = getattr(dim, "label_bbox", None)
1515
+ if spec is None or label is None:
1516
+ return None
1517
+ dx = float(spec.p2[0]) - float(spec.p1[0])
1518
+ dy = float(spec.p2[1]) - float(spec.p1[1])
1519
+ if min(abs(dx), abs(dy)) > 0.1 or max(abs(dx), abs(dy)) <= 1e-9:
1520
+ return None # rotated labels need polygonal motion, not an AABB-axis solve
1521
+ axis = 1 if abs(dy) > abs(dx) else 0
1522
+ other = 1 - axis
1523
+ return axis, other, spec
1524
+
1525
+ infos = [_axis_info(dim) for _name, dim in original]
1526
+ if not any(info is not None for info in infos):
1527
+ return original
1528
+
1529
+ box_cache: dict[int, Any] = {}
1530
+ segments_cache: dict[int, Any] = {}
1531
+ tips_cache: dict[int, Any] = {}
1532
+
1533
+ def _box(dim):
1534
+ key = id(dim)
1535
+ if key in box_cache:
1536
+ return box_cache[key]
1537
+ raw = getattr(dim, "label_bbox", None)
1538
+ box_cache[key] = tuple(float(value) for value in raw) if raw is not None else None
1539
+ return box_cache[key]
1540
+
1541
+ def _segments(dim):
1542
+ key = id(dim)
1543
+ if key in segments_cache:
1544
+ return segments_cache[key]
1545
+ try:
1546
+ segments_cache[key] = tuple(
1547
+ (
1548
+ (float(first[0]), float(first[1])),
1549
+ (float(second[0]), float(second[1])),
1550
+ )
1551
+ for first, second in (getattr(dim, "segments", ()) or ())
1552
+ )
1553
+ except Exception: # noqa: BLE001 — optional metadata fails closed to lint
1554
+ segments_cache[key] = ()
1555
+ return segments_cache[key]
1556
+
1557
+ def _arrow_tips(dim, info):
1558
+ """Dimension terminator tips from its measured endpoints and dim-line ordinate.
1559
+
1560
+ Helpers expose line pieces but not the filled arrow triangles. Their tips remain
1561
+ exactly the two measured ordinates on the label's dimension line; treating the tip
1562
+ attachment as label-blocking catches the short-span arrow-through-digit case without
1563
+ inflating every shaft into a coarse full-geometry box.
1564
+ """
1565
+ key = id(dim)
1566
+ if key in tips_cache:
1567
+ return tips_cache[key]
1568
+ if info is None or (label := _box(dim)) is None:
1569
+ return ()
1570
+ axis, other, spec = info
1571
+ line = (label[other] + label[other + 2]) / 2.0
1572
+ result = []
1573
+ for point in (spec.p1, spec.p2):
1574
+ tip = [0.0, 0.0]
1575
+ tip[axis] = float(point[axis])
1576
+ tip[other] = line
1577
+ result.append(tuple(tip))
1578
+ tips_cache[key] = tuple(result)
1579
+ return tips_cache[key]
1580
+
1581
+ natural_obstacle_hits = [
1582
+ frozenset(
1583
+ obstacle_index
1584
+ for obstacle_index, obstacle in enumerate(obstacles)
1585
+ if label is not None and _boxes_overlap(label, obstacle)
1586
+ )
1587
+ for _name, dim in original
1588
+ for label in (_box(dim),)
1589
+ ]
1590
+
1591
+ def _conflicts(batch):
1592
+ """Stable conflict tokens; their count is the local solve's primary objective."""
1593
+ labels = [_box(dim) for _name, dim in batch]
1594
+ segs = [_segments(dim) for _name, dim in batch]
1595
+ regions = [
1596
+ crossable_region(label, item=dim, segments=segments)
1597
+ for (_name, dim), label, segments in zip(batch, labels, segs, strict=True)
1598
+ ]
1599
+ tips = [_arrow_tips(dim, info) for (_name, dim), info in zip(batch, infos, strict=True)]
1600
+ found: set[tuple] = set()
1601
+ for target, (label, region) in enumerate(zip(labels, regions, strict=True)):
1602
+ if label is None or region is None:
1603
+ continue
1604
+ # Helpers expose no arrow polygons. The foreign dimension's exact
1605
+ # attachment tips still participate, closing the line-metadata gap without
1606
+ # guessing the orientation of this dimension's own inside/outside arrows.
1607
+ for source, source_tips in enumerate(tips):
1608
+ if source == target:
1609
+ continue
1610
+ info = infos[target]
1611
+ if info is None:
1612
+ continue
1613
+ axis, other, _spec = info
1614
+ for tip_index, tip in enumerate(source_tips):
1615
+ if (
1616
+ label[axis] + _LABEL_INK_CLEARANCE_MM
1617
+ < tip[axis]
1618
+ < label[axis + 2] - _LABEL_INK_CLEARANCE_MM
1619
+ and label[other] - 1e-6 < tip[other] < label[other + 2] + 1e-6
1620
+ ):
1621
+ found.add(("arrow", source, tip_index, target))
1622
+ for source, source_segments in enumerate(segs):
1623
+ if source == target:
1624
+ continue # the helper deliberately cuts its own line around its label
1625
+ if crossing_length(source_segments, region) >= MIN_CROSSING_MM:
1626
+ found.add(("line", source, target))
1627
+ for obstacle_index, obstacle in enumerate(obstacles):
1628
+ if obstacle_index not in natural_obstacle_hits[target] and _boxes_overlap(
1629
+ label, obstacle
1630
+ ):
1631
+ found.add(("fixed", obstacle_index, target))
1632
+ for left, left_box in enumerate(labels):
1633
+ if left_box is None:
1634
+ continue
1635
+ for right in range(left + 1, len(labels)):
1636
+ right_box = labels[right]
1637
+ if right_box is not None and _boxes_overlap(left_box, right_box):
1638
+ found.add(("label", left, right))
1639
+ return frozenset(found)
1640
+
1641
+ natural_centres = []
1642
+ for (_name, dim), info in zip(original, infos, strict=True):
1643
+ label = _box(dim)
1644
+ natural_centres.append(
1645
+ None if info is None or label is None else (label[info[0]] + label[info[0] + 2]) / 2.0
1646
+ )
1647
+
1648
+ def _objective(batch):
1649
+ conflicts = _conflicts(batch)
1650
+ fixed_conflicts = sum(conflict[0] == "fixed" for conflict in conflicts)
1651
+ offsets = []
1652
+ tier_offsets = []
1653
+ for (_name, dim), info, natural in zip(batch, infos, natural_centres, strict=True):
1654
+ label = _box(dim)
1655
+ offsets.append(
1656
+ 0.0
1657
+ if info is None or natural is None or label is None
1658
+ else abs((label[info[0]] + label[info[0] + 2]) / 2.0 - natural)
1659
+ )
1660
+ for (_name, dim), (_original_name, original_dim) in zip(batch, original, strict=True):
1661
+ spec = getattr(dim, "_dw_spec", None)
1662
+ original_spec = getattr(original_dim, "_dw_spec", None)
1663
+ tier_offsets.append(
1664
+ 0.0
1665
+ if spec is None or original_spec is None
1666
+ else abs(float(spec.distance) - float(original_spec.distance))
1667
+ )
1668
+ return (
1669
+ fixed_conflicts,
1670
+ len(conflicts),
1671
+ sum(value > 1e-9 for value in tier_offsets),
1672
+ round(sum(offsets), 9),
1673
+ round(max(offsets, default=0.0), 9),
1674
+ tuple(round(value, 9) for value in offsets),
1675
+ ), conflicts
1676
+
1677
+ current = list(original)
1678
+ current_objective, conflicts = _objective(current)
1679
+ if not conflicts:
1680
+ return current # overwhelmingly common path: no extra Dimension construction
1681
+
1682
+ cache: dict[tuple[int, float, float], Any] = {}
1683
+
1684
+ def _rebuild(index, centre, distance_delta=0.0):
1685
+ key = (index, round(centre, 6), round(distance_delta, 6))
1686
+ if key in cache:
1687
+ return cache[key]
1688
+ name, dim = original[index]
1689
+ info = infos[index]
1690
+ natural = natural_centres[index]
1691
+ if info is None or natural is None:
1692
+ return dim
1693
+ axis, _other, spec = info
1694
+ direction = 1.0 if float(spec.p2[axis]) >= float(spec.p1[axis]) else -1.0
1695
+ kwargs = dict(spec.kwargs)
1696
+ kwargs["label_offset_x"] = (
1697
+ kwargs.get("label_offset_x", 0.0) + (centre - natural) * direction
1698
+ )
1699
+ rebuilt = _dim(
1700
+ spec.p1,
1701
+ spec.p2,
1702
+ spec.side,
1703
+ spec.distance + distance_delta,
1704
+ spec.draft,
1705
+ **kwargs,
1706
+ )
1707
+ # The producer attaches semantic evidence before the corridor commits the
1708
+ # survivor. Rebuilding only the rendered Dimension must not erase that evidence:
1709
+ # hole-table escalation and coverage lint read these riders from the final object.
1710
+ # Copy semantic namespaces, never helper geometry internals; `_dw_spec` belongs to
1711
+ # the rebuilt placement and must remain the one `_dim` just created.
1712
+ for attr, value in vars(dim).items():
1713
+ if attr.startswith("covers_") or (attr.startswith("_dw_") and attr != "_dw_spec"):
1714
+ setattr(rebuilt, attr, value)
1715
+ cache[key] = rebuilt
1716
+ return rebuilt
1717
+
1718
+ def _centres_for(index, batch):
1719
+ info = infos[index]
1720
+ target = batch[index][1]
1721
+ label = _box(target)
1722
+ if info is None or label is None:
1723
+ return ()
1724
+ axis, other, spec = info
1725
+ current_centre = (label[axis] + label[axis + 2]) / 2.0
1726
+ half = (label[axis + 2] - label[axis]) / 2.0
1727
+ # The centre remains attached to its own measured span. A half-millimetre
1728
+ # overhang admits the minimum clear position for text wider than a short span.
1729
+ lo, hi = sorted((float(spec.p1[axis]), float(spec.p2[axis])))
1730
+ lo -= MIN_CROSSING_MM
1731
+ hi += MIN_CROSSING_MM
1732
+ if page is not None:
1733
+ lo = max(lo, float(page[axis]) + half)
1734
+ hi = min(hi, float(page[axis + 2]) - half)
1735
+ if lo > hi:
1736
+ return ()
1737
+ choices = {min(max(current_centre, lo), hi), lo, hi}
1738
+ for source, ((_name, dim), source_info) in enumerate(zip(batch, infos, strict=True)):
1739
+ source_label = _box(dim)
1740
+ if source != index and source_label is not None:
1741
+ # Existing labels are obstacles too; moving out of line-work must not
1742
+ # exchange one illegibility defect for text-on-text.
1743
+ if (
1744
+ source_label[other] < label[other + 2]
1745
+ and source_label[other + 2] > label[other]
1746
+ ):
1747
+ choices.add(source_label[axis] - half - _LABEL_INK_CLEARANCE_MM)
1748
+ choices.add(source_label[axis + 2] + half + _LABEL_INK_CLEARANCE_MM)
1749
+ if source == index:
1750
+ continue
1751
+ for tip in _arrow_tips(dim, source_info):
1752
+ if label[other] - 1e-6 <= tip[other] <= label[other + 2] + 1e-6:
1753
+ choices.add(tip[axis] - half - _LABEL_INK_CLEARANCE_MM)
1754
+ choices.add(tip[axis] + half + _LABEL_INK_CLEARANCE_MM)
1755
+ # Clip each foreign segment only to this label's fixed-axis band. The
1756
+ # resulting projection is the exact interval its centre must clear.
1757
+ band = [-1e9, -1e9, 1e9, 1e9]
1758
+ band[other], band[other + 2] = label[other], label[other + 2]
1759
+ for segment in _segments(dim):
1760
+ clipped = _segment_clip_extent(segment[0], segment[1], tuple(band), pad=0.0)
1761
+ if clipped is None:
1762
+ continue
1763
+ choices.add(clipped[axis] - half - _LABEL_INK_CLEARANCE_MM)
1764
+ choices.add(clipped[axis + 2] + half + _LABEL_INK_CLEARANCE_MM)
1765
+ bounded = {min(max(value, lo), hi) for value in choices if math.isfinite(value)}
1766
+ # Bound exceptional work. Nearest candidates are the meaningful drafting
1767
+ # alternatives; the two association bounds remain present explicitly.
1768
+ nearest = sorted(bounded, key=lambda value: (abs(value - current_centre), value))[:16]
1769
+ return tuple(dict.fromkeys([*nearest, lo, hi]))
1770
+
1771
+ # Deterministic steepest descent. Every accepted move strictly improves the
1772
+ # lexicographic objective, so it terminates even when the layout is infeasible.
1773
+ for _iteration in range(max(1, 2 * len(current))):
1774
+ involved: set[int] = set()
1775
+ for conflict in conflicts:
1776
+ if conflict[0] == "arrow":
1777
+ involved.update((conflict[1], conflict[3])) # source + crossed label
1778
+ elif conflict[0] == "line":
1779
+ involved.update((conflict[1], conflict[2])) # source + crossed label
1780
+ elif conflict[0] == "fixed":
1781
+ involved.add(conflict[2])
1782
+ else: # label/label
1783
+ involved.update((conflict[1], conflict[2]))
1784
+ best = None
1785
+ for index in sorted(involved):
1786
+ name, _dim_obj = original[index]
1787
+ if name in immutable or infos[index] is None:
1788
+ continue
1789
+ distances = (0.0,) if perpendicular_step is None else (0.0, perpendicular_step)
1790
+ for distance_delta in distances:
1791
+ for centre in _centres_for(index, current):
1792
+ rebuilt = _rebuild(index, centre, distance_delta)
1793
+ if page is not None:
1794
+ box = _anno_box(rebuilt)
1795
+ if box is not None and not (
1796
+ page[0] <= box[0]
1797
+ and box[2] <= page[2]
1798
+ and page[1] <= box[1]
1799
+ and box[3] <= page[3]
1800
+ ):
1801
+ continue
1802
+ trial = list(current)
1803
+ trial[index] = (name, rebuilt)
1804
+ objective, trial_conflicts = _objective(trial)
1805
+ key = (
1806
+ objective,
1807
+ index,
1808
+ round(distance_delta, 9),
1809
+ round(centre, 9),
1810
+ )
1811
+ if objective < current_objective and (best is None or key < best[0]):
1812
+ best = (key, trial, objective, trial_conflicts)
1813
+ if best is None:
1814
+ break
1815
+ _key, current, current_objective, conflicts = best
1816
+ if not conflicts:
1817
+ break
1818
+ return current
1819
+
1820
+
1461
1821
  # ── The corridor priority ladder (#894) ───────────────────────────────────────
1462
1822
  # `CorridorCandidate.priority` is an over-capacity survival rank, and a rank only
1463
1823
  # means anything RELATIVE to the other rungs. Defining the whole ladder here — beside
@@ -2126,6 +2486,19 @@ def place_strip_candidates(
2126
2486
  tp["free_segments"] = [list(s) for s in segs]
2127
2487
  todo = list(cands)
2128
2488
 
2489
+ def _real_box_conflict(name, real):
2490
+ """Return the hard-obstacle reason for a built survivor, if any."""
2491
+ if real is None:
2492
+ return None
2493
+ if not force and _box_hits(real, blockers):
2494
+ return "real_box_corridor_blocked"
2495
+ fb = (forbid or {}).get(name)
2496
+ if fb is not None and _box_hits(real, (fb,)):
2497
+ return "real_box_forbid"
2498
+ if _box_hits(real, out_of_band):
2499
+ return "real_box_out_of_band"
2500
+ return None
2501
+
2129
2502
  def _take_for_segment(items, n):
2130
2503
  if len(items) <= n:
2131
2504
  return items, []
@@ -2210,6 +2583,7 @@ def place_strip_candidates(
2210
2583
  accepted.append(((name, build), pos))
2211
2584
  return accepted, rejected
2212
2585
 
2586
+ solved = []
2213
2587
  for seg_lo, seg_hi in segs:
2214
2588
  if not todo:
2215
2589
  break
@@ -2231,43 +2605,60 @@ def place_strip_candidates(
2231
2605
  for (name, build), pos in accepted:
2232
2606
  dim = build(pos)
2233
2607
  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
2608
+ if reason := _real_box_conflict(name, real):
2609
+ if tp is not None:
2610
+ tp["rejected"].append({"name": name, "reason": reason})
2611
+ rejected_total.append((name, build))
2612
+ continue
2257
2613
  placed.append((name, dim))
2258
2614
  if tp is not None:
2259
2615
  tp["placed"].append({"name": name, "pos": pos})
2260
2616
  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
- )
2617
+ solved.extend(placed)
2618
+
2619
+ if len(solved) > 1:
2620
+ assert len({name for name, _dim_obj in solved}) == len(solved), (
2621
+ "strip survivor names must be unique before label selection"
2622
+ )
2623
+ natural_solved = dict(solved)
2624
+ adjusted = prevent_dimension_label_ink(
2625
+ solved,
2626
+ page=(
2627
+ (
2628
+ _MARGIN,
2629
+ _MARGIN,
2630
+ float(dwg.page_w) - _MARGIN,
2631
+ float(dwg.page_h) - _MARGIN,
2632
+ )
2633
+ if hasattr(dwg, "page_w") and hasattr(dwg, "page_h")
2634
+ else None
2635
+ ),
2636
+ immutable={name for name, _dim_obj in solved if (anchored or {}).get(name, False)},
2637
+ )
2638
+ # A label shift normally stays inside the dimension's measured span and
2639
+ # therefore inside its already-validated footprint. Keep the validation
2640
+ # contract explicit nevertheless: if an unusual helper/style grows the real
2641
+ # box into a hard obstacle, retain the natural survivor. The lint backstop then
2642
+ # records the infeasible ink conflict instead of this prevention step trading
2643
+ # it for a structural collision.
2644
+ solved = []
2645
+ for name, dim in adjusted:
2646
+ natural = natural_solved[name]
2647
+ if dim is natural:
2648
+ solved.append((name, dim))
2649
+ continue
2650
+ real = _geom_box(dim)
2651
+ solved.append((name, natural if _real_box_conflict(name, real) else dim))
2652
+ for name, dim in solved:
2653
+ # Record feature provenance (ADR 0010): the drain-time seam for corridor-placed
2654
+ # dims — `features` maps this batch's names to their source IR feature.
2655
+ ctx.place(
2656
+ dim,
2657
+ name,
2658
+ view=view,
2659
+ feature=(features or {}).get(name),
2660
+ measurement=(measurements or {}).get(name),
2661
+ )
2271
2662
  if tp is not None:
2272
2663
  tp["unplaced"] = [n for n, _ in todo]
2273
2664
  trace.end_pass(tp) # folds a standalone pass's items; no-op when corridor-nested