draftwright 0.4.0__tar.gz → 0.4.1__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.
- {draftwright-0.4.0 → draftwright-0.4.1}/CHANGELOG.md +91 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/PKG-INFO +12 -3
- {draftwright-0.4.0 → draftwright-0.4.1}/README.md +11 -2
- {draftwright-0.4.0 → draftwright-0.4.1}/docs/adr/README.md +2 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/pyproject.toml +18 -3
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/__init__.py +7 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/_core.py +177 -42
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/_geometry.py +97 -0
- draftwright-0.4.1/src/draftwright/_warnings.py +39 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/analysis.py +117 -28
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/annotations/_common.py +32 -5
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/annotations/from_model.py +367 -56
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/annotations/holes.py +202 -39
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/annotations/orchestrator.py +47 -6
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/annotations/sections.py +108 -71
- draftwright-0.4.1/src/draftwright/audit.py +249 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/builder.py +36 -4
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/compose.py +38 -7
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/drawing.py +281 -19
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/linting/__init__.py +10 -0
- draftwright-0.4.1/src/draftwright/linting/channel_coverage.py +190 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/linting/coverage.py +485 -11
- draftwright-0.4.1/src/draftwright/linting/flat_coverage.py +207 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/linting/issues.py +4 -0
- draftwright-0.4.1/src/draftwright/linting/profiled_bore_coverage.py +122 -0
- draftwright-0.4.1/src/draftwright/linting/slot_coverage.py +315 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/model/__init__.py +8 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/model/callout.py +33 -2
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/model/compiled.py +150 -27
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/model/declare.py +245 -18
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/model/detect.py +103 -13
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/model/ir.py +194 -4
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/model/planner.py +157 -40
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/projection.py +9 -1
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/recognition/__init__.py +29 -5
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/recognition/flats.py +103 -3
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/recognition/levels.py +170 -44
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/recognition/pads.py +5 -4
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/recognition/plates.py +5 -0
- draftwright-0.4.1/src/draftwright/recognition/profiled_bores.py +368 -0
- draftwright-0.4.1/src/draftwright/recognition/result.py +277 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/recognition/slots.py +127 -19
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/registry.py +109 -1
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/repair.py +11 -3
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/score.py +2 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/sheet.py +81 -20
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/sheet_emit.py +77 -2
- draftwright-0.4.1/tests/conftest.py +84 -0
- draftwright-0.4.1/tests/fixtures/if_step_flat_across_cylinder.step +189 -0
- draftwright-0.4.1/tests/fixtures/issue_1058_wheel_rh.step +64624 -0
- draftwright-0.4.1/tests/fixtures/issue_909_basic_part_design_017_body.step +465 -0
- draftwright-0.4.1/tests/fixtures/issue_915_case_study_2.step +3527 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/refactor_golden/pocketed.json +2 -2
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_add_dimension.py +36 -10
- draftwright-0.4.1/tests/test_audit_differential.py +768 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_compiled_plan_boundary.py +24 -0
- draftwright-0.4.1/tests/test_counting_calls.py +117 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_declare.py +54 -2
- draftwright-0.4.1/tests/test_declared_recognition_gate.py +542 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_detect_once.py +38 -36
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_detect_registry.py +12 -3
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_drawing_encapsulation.py +28 -4
- draftwright-0.4.1/tests/test_flat_completeness.py +330 -0
- draftwright-0.4.1/tests/test_flat_stock_identity.py +240 -0
- draftwright-0.4.1/tests/test_flat_stock_opposition.py +63 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_import_boundaries.py +9 -0
- draftwright-0.4.1/tests/test_issue_1058_wheel_profile.py +676 -0
- draftwright-0.4.1/tests/test_issue_909_sloped_profile.py +150 -0
- draftwright-0.4.1/tests/test_issue_915_dense_case.py +108 -0
- draftwright-0.4.1/tests/test_issue_916_pocket_leader.py +102 -0
- draftwright-0.4.1/tests/test_issue_917_open_channel.py +189 -0
- draftwright-0.4.1/tests/test_location_vocabulary.py +400 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_make_drawing.py +278 -32
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_parameter_id.py +27 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_part_model.py +2 -2
- draftwright-0.4.1/tests/test_principal_profile_classifier.py +122 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_recogniser_contract.py +35 -4
- draftwright-0.4.1/tests/test_recognition_manifest.py +567 -0
- draftwright-0.4.1/tests/test_recognition_result.py +340 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_registry.py +93 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_render_seam.py +1 -1
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_script_detail_parity.py +42 -4
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_sheet_emit.py +66 -1
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_sheet_identity_invariant.py +2 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_slanted_blind_step.py +15 -7
- draftwright-0.4.1/tests/test_slot_completeness.py +595 -0
- draftwright-0.4.1/tests/test_soft_deprecation.py +221 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_strip_layout.py +5 -5
- draftwright-0.4.1/tests/test_suppression_ledger.py +380 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/.gitignore +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/LICENSE +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/skills/SKILL.md +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/annotate.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/annotations/__init__.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/annotations/balloons.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/cli.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/export.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/fits.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/fonts/__init__.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/intents.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/layout.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/linting/structural.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/linting/suggest.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/make_drawing.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/pmi.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/py.typed +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/recognition/_features.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/recognition/_record.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/recognition/chamfers.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/recognition/countersinks.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/recognition/fillets.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/recognition/grooves.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/src/draftwright/recognition/turned.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/_kernel.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/_layout_sig.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/fixtures/grm03_thumbwheel_drive_screw.step +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/fixtures/tuner_jig_blind_obround_pockets.step +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/refactor_golden/bracket_section.json +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/refactor_golden/centered_rebate.json +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/refactor_golden/chamfered.json +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/refactor_golden/filleted.json +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/refactor_golden/flange_dense.json +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/refactor_golden/grid_plate.json +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/refactor_golden/grooved_shaft.json +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/refactor_golden/hex_bar.json +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/refactor_golden/holed_slot.json +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/refactor_golden/prismatic_ladder.json +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/refactor_golden/scattered_plate.json +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/refactor_golden/side_drilled.json +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/refactor_golden/turned_stepped.json +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_agents_guide.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_architecture_docs.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_balloon_ring_standoff.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_carve_free_position_callers.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_dense_sheet_canary.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_deprecation_dates.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_dimension_role_vocabulary.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_e2e_slice.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_e2e_standards.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_export_dxf_zoom.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_fillets_adjacency.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_fits.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_gdt_placement.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_grid_lattice_convention.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_issue_885_prismatic_coverage.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_label_provenance.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_layout.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_layout_cleanliness.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_layout_hypothesis.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_layout_property.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_lint_box_cache.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_lint_reconciliation.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_lint_structural.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_linting.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_note_verb.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_object_aspects.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_occupancy_boxes.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_pitch_dim_footprint.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_pmi.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_pocket_pattern.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_pocket_pattern_recognition.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_private_test_attr_reads.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_private_test_imports.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_recognition.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_refactor_golden.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_score.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_sheet_gdt.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_sheet_notes.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_sheet_of.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_sheet_section.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_sheet_tables.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_slot_pattern.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_slot_pattern_recognition.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_solve_trace.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_suppression_marks.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_tolerances.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_turned_steps.py +0 -0
- {draftwright-0.4.0 → draftwright-0.4.1}/tests/test_witness_label_reconciliation.py +0 -0
|
@@ -2,6 +2,97 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## v0.4.1 — 2026-08-07
|
|
6
|
+
|
|
7
|
+
**The recognition trust release.** Automatic drawings now fail closed on unsupported inner
|
|
8
|
+
profiles, prove semantic coverage for flats, slots and patterns, and retain the measurement
|
|
9
|
+
identity needed to explain placed, suppressed, dropped and missing definitions. Several real
|
|
10
|
+
STEP case studies also move from clean-looking but incomplete output to complete drawings or
|
|
11
|
+
actionable lint.
|
|
12
|
+
|
|
13
|
+
### Discouraged (supported)
|
|
14
|
+
|
|
15
|
+
- **`Sheet.auto_dimensions()` and `Sheet.add_dimension()` now emit
|
|
16
|
+
`SoftDeprecationWarning`** (#1043). They remain supported with no removal planned; the
|
|
17
|
+
warning steers declared scripts toward `authored_dimensions()` plus explicit
|
|
18
|
+
`dimension(feature, parameter_id)` lines, where omission can mean suppression. Automatic
|
|
19
|
+
`build_drawing(part)` and the `Sheet.from_part(part)` on-ramp remain silent. See
|
|
20
|
+
`docs/deprecations.md` for filtering and migration guidance.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **Finished drawings expose why measurements were omitted and, on covered placement paths,
|
|
25
|
+
what an annotation measures** (#996/#1002). `Drawing.suppressions()` returns the compiler's
|
|
26
|
+
omission ledger, `Drawing.measurement_keys(name)` exposes recorded measurement provenance, and
|
|
27
|
+
`draftwright.audit.diff_builds(before, after)` reports losses, substitutions and candidate
|
|
28
|
+
suppression explanations. The differential is deliberately a triage aid, not a proof where
|
|
29
|
+
renderer identity remains incomplete.
|
|
30
|
+
|
|
31
|
+
- **Physical-completeness lint now follows semantic correspondence for flats, lone slots and
|
|
32
|
+
slot patterns** (#1018). Coverage is joined through recognition, IR and compiler identities
|
|
33
|
+
rather than inferred from label text or page geometry, and distinguishes authored
|
|
34
|
+
suppression, placement drops, missing definitions and unverifiable provenance.
|
|
35
|
+
|
|
36
|
+
- **Full-span floored open channels are recognised and dimensioned as first-class features**
|
|
37
|
+
(#917). Automatic and declared drawings state the independent overall extent, one wall and
|
|
38
|
+
channel width, suppress the derived opposite wall, and report any missing member of that
|
|
39
|
+
defining chain.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- **Square and near-square parts retain both independent planar extents** (#997). A plain
|
|
44
|
+
square plate could previously lose both plan dimensions, while parts up to five percent off
|
|
45
|
+
square could lose one and be silently represented as square. The unsafe suppression rule is
|
|
46
|
+
removed; explicit square notation remains tracked separately in #918.
|
|
47
|
+
|
|
48
|
+
- **Machined flats retain their physical stock identity from recognition through placement
|
|
49
|
+
and completeness lint** (#1013/#1015/#1034/#1036). Flats on separate parallel, coaxial or
|
|
50
|
+
slanted stock no longer collapse into one definition or borrow an opposite face from another
|
|
51
|
+
stock region. Independent callouts receive clear-margin candidates, while double-D faces on
|
|
52
|
+
one stock still form one A/F requirement.
|
|
53
|
+
|
|
54
|
+
- **Dense callout and step-detail cases keep their complete defining set** (#915). Hole
|
|
55
|
+
callout batches are reconciled as a queue, detail views fit their actual aspect, step levels
|
|
56
|
+
retain the supporting-face correspondence needed for truthful crops, and a detail redraws
|
|
57
|
+
only rungs omitted from its parent view.
|
|
58
|
+
|
|
59
|
+
- **Pocket leaders start on the physical opening rim** (#916), removing the spurious inner
|
|
60
|
+
silhouette crossing while preserving the solved label corridor and approved dimensions on
|
|
61
|
+
X-, Y- and Z-depth pockets.
|
|
62
|
+
|
|
63
|
+
- **Wedge-mounted raised pads survive recognition** (#909). Lower ledges that touch only in
|
|
64
|
+
plan no longer make the recogniser discard a valid upper pad; true staircase tiers remain
|
|
65
|
+
excluded.
|
|
66
|
+
|
|
67
|
+
- **Annotation-dense detail placement no longer stalls the build** (#1065). Detail views
|
|
68
|
+
avoid every decomposed annotation shaft, witness, label and item footprint; CTC-02
|
|
69
|
+
supplies 571 such boxes. The free-rectangle search previously treated that set as tiny
|
|
70
|
+
and admitted roughly 34 billion candidate rectangles, exhausting the slow tier's
|
|
71
|
+
ten-minute test budget before export. A compressed-coordinate sweep preserves the same
|
|
72
|
+
deterministic winning rectangle while completing the real search in seconds.
|
|
73
|
+
|
|
74
|
+
- **Through double-D bores are recognised and called out as first-class hole profiles**
|
|
75
|
+
(#1061). Automatic and declared builds preserve the parent-circle diameter, A/F size,
|
|
76
|
+
orientation and depth, and render one compound `DOUBLE-D ... A/F` bore callout. Physical
|
|
77
|
+
critique now accepts that supported inner profile while retaining a separate warning for
|
|
78
|
+
13 evenly spaced common-circle arcs on the unresolved outer boundary; it does not infer a
|
|
79
|
+
repeating full profile or a gear standard.
|
|
80
|
+
|
|
81
|
+
- **Unsupported internal profiles no longer pass physical-completeness lint silently**
|
|
82
|
+
(#1058). Inner loops on principal boundary faces are checked against the current
|
|
83
|
+
circular-hole, rectangular-opening, true-obround, and proven through-double-D vocabulary.
|
|
84
|
+
Profiles outside it now produce
|
|
85
|
+
`unrecognised_defining_geometry` and reduce the lint score instead of allowing an
|
|
86
|
+
envelope-only drawing to score 1.0.
|
|
87
|
+
|
|
88
|
+
### Changed
|
|
89
|
+
|
|
90
|
+
- **Crowded prismatic step dimensions recover into an enlarged detail view by default**
|
|
91
|
+
(#909). The automatic `build_drawing` / `make_drawing` / CLI paths and generated
|
|
92
|
+
`Sheet` scripts now preserve the omitted dimensions instead of returning
|
|
93
|
+
`step_dim_dropped` unless the caller remembered `detail_view=True`. Pass
|
|
94
|
+
`detail_view=False` to retain the parent-view-only behavior and its lint warning.
|
|
95
|
+
|
|
5
96
|
## v0.4.0 — 2026-08-02
|
|
6
97
|
|
|
7
98
|
**The compat-exit release.** Every surface whose documented removal target was 0.4.0 is gone,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: draftwright
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
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
|
|
@@ -771,9 +771,12 @@ from draftwright import build_drawing
|
|
|
771
771
|
|
|
772
772
|
dwg = build_drawing(part, title="Mounting Plate")
|
|
773
773
|
issues = dwg.lint() # list[LintIssue] — coverage, page bounds, ISO
|
|
774
|
-
|
|
774
|
+
paths = dwg.export("my_part", formats=("svg", "dxf")) # {"svg": ..., "dxf": ...}
|
|
775
775
|
```
|
|
776
776
|
|
|
777
|
+
`export()` with no `formats=` returns the old `(svg, dxf)` tuple; that shape is deprecated
|
|
778
|
+
and removed in 0.5.0 (see [docs/deprecations.md](docs/deprecations.md)).
|
|
779
|
+
|
|
777
780
|
### Declarative drawings — reference features, declare intent
|
|
778
781
|
|
|
779
782
|
Instead of relying on detection, **reference the build123d objects you built** and declare
|
|
@@ -789,10 +792,16 @@ plate = Box(120, 80, 20)
|
|
|
789
792
|
bore = Pos(0, 0, 0) * Cylinder(4, 20)
|
|
790
793
|
|
|
791
794
|
sheet = Sheet(plate - bore, title="Plate", number="DWG-002")
|
|
792
|
-
sheet.envelope()
|
|
795
|
+
env = sheet.envelope()
|
|
793
796
|
sheet.datum("A", plate.faces().sort_by()[-1]) # datum A on the top face
|
|
794
797
|
hole = sheet.hole(bore).finish("1.6").note("M3x0.5 TAP") # ⌀8 bore, Ra 1.6, tapped
|
|
795
798
|
sheet.control(hole).position(0.1, to="A", diameter=True) # ⌀0.1 position wrt A
|
|
799
|
+
|
|
800
|
+
sheet.authored_dimensions() # THIS is the complete set
|
|
801
|
+
sheet.dimension(env, "width.length") # (ADR 0016: a build says where
|
|
802
|
+
sheet.dimension(env, "height.length") # its dimensions come from, and
|
|
803
|
+
sheet.dimension(hole, "bore.diameter") # omitting one means suppress it)
|
|
804
|
+
|
|
796
805
|
sheet.export("plate") # writes plate.pdf
|
|
797
806
|
```
|
|
798
807
|
|
|
@@ -75,9 +75,12 @@ from draftwright import build_drawing
|
|
|
75
75
|
|
|
76
76
|
dwg = build_drawing(part, title="Mounting Plate")
|
|
77
77
|
issues = dwg.lint() # list[LintIssue] — coverage, page bounds, ISO
|
|
78
|
-
|
|
78
|
+
paths = dwg.export("my_part", formats=("svg", "dxf")) # {"svg": ..., "dxf": ...}
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
+
`export()` with no `formats=` returns the old `(svg, dxf)` tuple; that shape is deprecated
|
|
82
|
+
and removed in 0.5.0 (see [docs/deprecations.md](docs/deprecations.md)).
|
|
83
|
+
|
|
81
84
|
### Declarative drawings — reference features, declare intent
|
|
82
85
|
|
|
83
86
|
Instead of relying on detection, **reference the build123d objects you built** and declare
|
|
@@ -93,10 +96,16 @@ plate = Box(120, 80, 20)
|
|
|
93
96
|
bore = Pos(0, 0, 0) * Cylinder(4, 20)
|
|
94
97
|
|
|
95
98
|
sheet = Sheet(plate - bore, title="Plate", number="DWG-002")
|
|
96
|
-
sheet.envelope()
|
|
99
|
+
env = sheet.envelope()
|
|
97
100
|
sheet.datum("A", plate.faces().sort_by()[-1]) # datum A on the top face
|
|
98
101
|
hole = sheet.hole(bore).finish("1.6").note("M3x0.5 TAP") # ⌀8 bore, Ra 1.6, tapped
|
|
99
102
|
sheet.control(hole).position(0.1, to="A", diameter=True) # ⌀0.1 position wrt A
|
|
103
|
+
|
|
104
|
+
sheet.authored_dimensions() # THIS is the complete set
|
|
105
|
+
sheet.dimension(env, "width.length") # (ADR 0016: a build says where
|
|
106
|
+
sheet.dimension(env, "height.length") # its dimensions come from, and
|
|
107
|
+
sheet.dimension(hole, "bore.diameter") # omitting one means suppress it)
|
|
108
|
+
|
|
100
109
|
sheet.export("plate") # writes plate.pdf
|
|
101
110
|
```
|
|
102
111
|
|
|
@@ -33,6 +33,7 @@ architecture** table; open retired or superseded records only for design history
|
|
|
33
33
|
| [0014](0014-collect-then-solve-annotation-placement.md) | Collect-then-solve annotation placement (as built) | Collect, select, assign, and deterministically solve annotations per corridor before rendering. | Accepted; supersedes 0009 | `test_carve_free_position_callers.py`, `test_strip_layout.py`, `test_layout_property.py`, `test_import_boundaries.py` |
|
|
34
34
|
| [0015](0015-part-drawing-compiler-as-built.md) | The part-drawing compiler, as built | Use one detected-or-declared feature IR and planner-fed dimension groups as the compiler waist. | Accepted; supersedes 0008 | `test_part_model.py`, `test_detect_once.py`, `test_import_boundaries.py` |
|
|
35
35
|
| [0016](0016-declared-dimensioning-intent.md) | Declared dimensioning intent: capture what to measure, let the engine place it | Declare which measurements matter as scale-independent intent routed through the planner and corridor solve; never hardcode dimension geometry. | Accepted; epic #867 complete; phase 6 landed (#940) | `test_compiled_plan_boundary.py`, `test_label_provenance.py`, `test_sheet_emit.py`, `test_add_dimension.py` |
|
|
36
|
+
| [0017](0017-recognition-inventory-correspondence-and-measurement-provenance.md) | One recognition result per run; correspondence is evidence-gated | Produce one explicit recognition result owned by `BuildState`; require vertical-slice evidence before generalising correspondence, identity, requirements, outcomes, or reconciliation. | Accepted; ownership phase landed, extensions gated by #1018 | `test_recognition_manifest.py`, `test_recognition_result.py`, `test_declared_recognition_gate.py` |
|
|
36
37
|
|
|
37
38
|
## Historical records
|
|
38
39
|
|
|
@@ -49,5 +50,6 @@ architecture** table; open retired or superseded records only for design history
|
|
|
49
50
|
- Layout and placement: 0004 → 0014 → 0012.
|
|
50
51
|
- Declared intent and the editable surface: 0001 → 0011 → 0012 → 0016.
|
|
51
52
|
- Quality and correction: 0002, with provenance from 0010.
|
|
53
|
+
- Recognition correspondence and completeness: 0007 → 0013 → 0015 → 0017.
|
|
52
54
|
|
|
53
55
|
Tracking issue: #745.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "draftwright"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.1"
|
|
8
8
|
description = "Automated technical-drawing generation for build123d"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -80,6 +80,18 @@ markers = [
|
|
|
80
80
|
"slow: heavy end-to-end CTC fixture builds (deselected by default)",
|
|
81
81
|
"smoke: curated build-light subset for a fast local check (~30 s)",
|
|
82
82
|
]
|
|
83
|
+
# `SoftDeprecationWarning` marks a surface that is discouraged but SUPPORTED and not going
|
|
84
|
+
# away (#1043). The suite exercises `Sheet.auto_dimensions()` in ~170 places, deliberately —
|
|
85
|
+
# it is still a supported path and its behaviour must keep being tested. Migrating those to
|
|
86
|
+
# authored dimensions would be churn that tests less, so the category is silenced here rather
|
|
87
|
+
# than at ~170 call sites.
|
|
88
|
+
#
|
|
89
|
+
# This does NOT weaken the guard: `tests/test_soft_deprecation.py` asserts the warning with
|
|
90
|
+
# `pytest.warns`, which fires regardless of this filter. Real `DeprecationWarning`s are
|
|
91
|
+
# untouched and still carry removal dates (`tests/test_deprecation_dates.py`).
|
|
92
|
+
filterwarnings = [
|
|
93
|
+
"ignore::draftwright.SoftDeprecationWarning",
|
|
94
|
+
]
|
|
83
95
|
# Coverage is a CI concern (it adds ~13% locally and instruments every line); the
|
|
84
96
|
# CI workflow passes the --cov flags explicitly. Keep the local default lean.
|
|
85
97
|
addopts = "--tb=short -m 'not slow'"
|
|
@@ -87,6 +99,9 @@ addopts = "--tb=short -m 'not slow'"
|
|
|
87
99
|
[tool.coverage.run]
|
|
88
100
|
source = ["src/draftwright"]
|
|
89
101
|
omit = ["src/draftwright/__init__.py"]
|
|
102
|
+
# Keep XML paths anchored at the repository root so local diff coverage and Codecov
|
|
103
|
+
# measure the same changed source lines rather than relying on runner-specific paths.
|
|
104
|
+
relative_files = true
|
|
90
105
|
# Branch coverage makes the regression gate sensitive to untested decision paths,
|
|
91
106
|
# which matter more than raw statement execution in the recognition/layout code.
|
|
92
107
|
branch = true
|
|
@@ -94,8 +109,8 @@ branch = true
|
|
|
94
109
|
[tool.coverage.report]
|
|
95
110
|
# Baseline measured on the full fast tier on Linux/Python 3.13 (#825):
|
|
96
111
|
# 92.05% combined line+branch coverage (93.90% statements, 86.89% branches).
|
|
97
|
-
# Keep two points of
|
|
98
|
-
#
|
|
112
|
+
# Keep two points of headroom; ratchet upward only after the canonical coverage job
|
|
113
|
+
# has stayed above the proposed floor.
|
|
99
114
|
fail_under = 90
|
|
100
115
|
precision = 1
|
|
101
116
|
exclude_lines = [
|
|
@@ -32,6 +32,11 @@ _LAZY = {
|
|
|
32
32
|
"PmiRecord": "draftwright.pmi",
|
|
33
33
|
"extract_pmi": "draftwright.pmi",
|
|
34
34
|
"choose_scale": "draftwright.compose",
|
|
35
|
+
# A warning category users are told to filter must be importable without reaching into a
|
|
36
|
+
# private module (#1043 review) — and without paying for the CAD kernel. It lives in the
|
|
37
|
+
# dependency-free `_warnings` leaf for that second reason: defined in `_core` it cost ~6 s
|
|
38
|
+
# to reach, and the pytest filterwarnings entry naming it paid that on every invocation.
|
|
39
|
+
"SoftDeprecationWarning": "draftwright._warnings",
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
|
|
@@ -68,6 +73,7 @@ _sys.modules[__name__].__class__ = _DraftwrightModule
|
|
|
68
73
|
|
|
69
74
|
|
|
70
75
|
if TYPE_CHECKING: # static analysers / IDEs — no runtime import, no kernel cost
|
|
76
|
+
from draftwright._warnings import SoftDeprecationWarning
|
|
71
77
|
from draftwright.builder import build_drawing, make_drawing
|
|
72
78
|
from draftwright.compose import choose_scale
|
|
73
79
|
from draftwright.drawing import Drawing, FeatureInfo
|
|
@@ -85,6 +91,7 @@ def __dir__():
|
|
|
85
91
|
|
|
86
92
|
__all__ = [
|
|
87
93
|
"Drawing",
|
|
94
|
+
"SoftDeprecationWarning",
|
|
88
95
|
"FeatureInfo",
|
|
89
96
|
"PmiRecord",
|
|
90
97
|
"Sheet",
|
|
@@ -17,7 +17,7 @@ import functools
|
|
|
17
17
|
import logging
|
|
18
18
|
import math
|
|
19
19
|
import re
|
|
20
|
-
from bisect import bisect_right
|
|
20
|
+
from bisect import bisect_left, bisect_right
|
|
21
21
|
from collections.abc import Callable
|
|
22
22
|
from dataclasses import dataclass
|
|
23
23
|
from pathlib import Path
|
|
@@ -26,7 +26,7 @@ from typing import TYPE_CHECKING, Literal
|
|
|
26
26
|
|
|
27
27
|
if TYPE_CHECKING:
|
|
28
28
|
from draftwright.compose import StripDepths
|
|
29
|
-
from draftwright.recognition import TurnedProfile
|
|
29
|
+
from draftwright.recognition import RecognitionResult, TurnedProfile
|
|
30
30
|
|
|
31
31
|
from build123d import Align, BoundBox, Compound, Edge, Location, Mode, Shape, Text, Vector
|
|
32
32
|
from build123d_drafting.helpers import (
|
|
@@ -49,18 +49,21 @@ from draftwright.layout import _greedy_strip_1d, _solve_strip_1d
|
|
|
49
49
|
_log = logging.getLogger(__name__)
|
|
50
50
|
|
|
51
51
|
|
|
52
|
-
def place_annotation(registry, items, obj, name=None, view=None, feature=None):
|
|
52
|
+
def place_annotation(registry, items, obj, name=None, view=None, feature=None, measurement=None):
|
|
53
53
|
"""The annotation-placement primitive (#817): register *obj* under *name* — replacing any
|
|
54
54
|
prior object of that name (dropped from the render list *items*) so a name maps to one
|
|
55
55
|
object — append it to *items*, and record its owning *view* + source *feature* in *registry*.
|
|
56
56
|
Shared by :meth:`Drawing._add` and :meth:`PlacementContext.place` so the render passes place
|
|
57
|
-
annotations without reaching into the ``Drawing``. Returns *obj*.
|
|
57
|
+
annotations without reaching into the ``Drawing``. Returns *obj*.
|
|
58
|
+
|
|
59
|
+
*measurement* is the `DimensionId` *obj* draws, where the caller holds one (#1002) —
|
|
60
|
+
one axis finer than *feature*, since a feature has several measurements."""
|
|
58
61
|
displaced = registry.named(name) if name is not None else None
|
|
59
62
|
if displaced is not None:
|
|
60
63
|
items.remove(displaced)
|
|
61
64
|
annotate(obj, name)
|
|
62
65
|
items.append(obj)
|
|
63
|
-
registry.add(obj, name, view, feature)
|
|
66
|
+
registry.add(obj, name, view, feature, measurement)
|
|
64
67
|
return obj
|
|
65
68
|
|
|
66
69
|
|
|
@@ -536,58 +539,184 @@ def _legible_locations(positions, scale):
|
|
|
536
539
|
return kept, n_too_close
|
|
537
540
|
|
|
538
541
|
|
|
539
|
-
|
|
542
|
+
class _FreeRunIndex:
|
|
543
|
+
"""Range-covered Y coordinates with the longest remaining free run.
|
|
544
|
+
|
|
545
|
+
Coordinate points are represented as zero-length cells between the ordinary
|
|
546
|
+
intervals. That lets a degenerate obstacle split a gap exactly as the open
|
|
547
|
+
AABB overlap predicate does, without assigning the point any physical width.
|
|
548
|
+
"""
|
|
549
|
+
|
|
550
|
+
def __init__(self, coordinates):
|
|
551
|
+
self.coordinates = coordinates
|
|
552
|
+
lengths = []
|
|
553
|
+
for i, value in enumerate(coordinates):
|
|
554
|
+
lengths.append(0.0)
|
|
555
|
+
if i + 1 < len(coordinates):
|
|
556
|
+
lengths.append(coordinates[i + 1] - value)
|
|
557
|
+
self._lengths = lengths
|
|
558
|
+
self._n = len(lengths)
|
|
559
|
+
size = 4 * self._n
|
|
560
|
+
self._cover = [0] * size
|
|
561
|
+
self._total = [0.0] * size
|
|
562
|
+
self._prefix = [0.0] * size
|
|
563
|
+
self._suffix = [0.0] * size
|
|
564
|
+
self._longest = [0.0] * size
|
|
565
|
+
self._all_free = [True] * size
|
|
566
|
+
self._build(1, 0, self._n)
|
|
567
|
+
|
|
568
|
+
def _build(self, node, lo, hi):
|
|
569
|
+
if hi - lo == 1:
|
|
570
|
+
length = self._lengths[lo]
|
|
571
|
+
self._total[node] = length
|
|
572
|
+
self._prefix[node] = length
|
|
573
|
+
self._suffix[node] = length
|
|
574
|
+
self._longest[node] = length
|
|
575
|
+
return
|
|
576
|
+
mid = (lo + hi) // 2
|
|
577
|
+
self._build(node * 2, lo, mid)
|
|
578
|
+
self._build(node * 2 + 1, mid, hi)
|
|
579
|
+
self._pull(node)
|
|
580
|
+
|
|
581
|
+
def _pull(self, node):
|
|
582
|
+
if self._cover[node]:
|
|
583
|
+
self._all_free[node] = False
|
|
584
|
+
self._prefix[node] = self._suffix[node] = self._longest[node] = 0.0
|
|
585
|
+
return
|
|
586
|
+
left, right = node * 2, node * 2 + 1
|
|
587
|
+
self._total[node] = self._total[left] + self._total[right]
|
|
588
|
+
self._all_free[node] = self._all_free[left] and self._all_free[right]
|
|
589
|
+
self._prefix[node] = self._prefix[left]
|
|
590
|
+
if self._all_free[left]:
|
|
591
|
+
self._prefix[node] += self._prefix[right]
|
|
592
|
+
self._suffix[node] = self._suffix[right]
|
|
593
|
+
if self._all_free[right]:
|
|
594
|
+
self._suffix[node] += self._suffix[left]
|
|
595
|
+
self._longest[node] = max(
|
|
596
|
+
self._longest[left],
|
|
597
|
+
self._suffix[left] + self._prefix[right],
|
|
598
|
+
self._longest[right],
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
@property
|
|
602
|
+
def longest(self):
|
|
603
|
+
return self._longest[1]
|
|
604
|
+
|
|
605
|
+
def block(self, lo, hi):
|
|
606
|
+
"""Add one obstacle interval in coordinate space."""
|
|
607
|
+
first = bisect_left(self.coordinates, lo)
|
|
608
|
+
last = bisect_left(self.coordinates, hi)
|
|
609
|
+
if lo == hi:
|
|
610
|
+
unit_lo, unit_hi = 2 * first, 2 * first + 1
|
|
611
|
+
else:
|
|
612
|
+
# Block the open span but not its endpoints: touching an obstacle is
|
|
613
|
+
# permitted by the placement AABB predicate.
|
|
614
|
+
unit_lo, unit_hi = 2 * first + 1, 2 * last
|
|
615
|
+
self._block(1, 0, self._n, unit_lo, unit_hi)
|
|
616
|
+
|
|
617
|
+
def _block(self, node, lo, hi, block_lo, block_hi):
|
|
618
|
+
if block_lo <= lo and hi <= block_hi:
|
|
619
|
+
self._cover[node] += 1
|
|
620
|
+
self._pull(node)
|
|
621
|
+
return
|
|
622
|
+
mid = (lo + hi) // 2
|
|
623
|
+
if block_lo < mid:
|
|
624
|
+
self._block(node * 2, lo, mid, block_lo, block_hi)
|
|
625
|
+
if mid < block_hi:
|
|
626
|
+
self._block(node * 2 + 1, mid, hi, block_lo, block_hi)
|
|
627
|
+
self._pull(node)
|
|
628
|
+
|
|
629
|
+
def first_run(self, minimum):
|
|
630
|
+
"""Lowest-Y free run at least *minimum* long."""
|
|
631
|
+
return self._first_run(1, 0, self._n, self.coordinates[0], minimum)
|
|
632
|
+
|
|
633
|
+
def _first_run(self, node, lo, hi, origin, minimum):
|
|
634
|
+
if hi - lo == 1:
|
|
635
|
+
return origin
|
|
636
|
+
left, right = node * 2, node * 2 + 1
|
|
637
|
+
mid = (lo + hi) // 2
|
|
638
|
+
if self._longest[left] >= minimum:
|
|
639
|
+
return self._first_run(left, lo, mid, origin, minimum)
|
|
640
|
+
if self._suffix[left] + self._prefix[right] >= minimum:
|
|
641
|
+
return origin + self._total[left] - self._suffix[left]
|
|
642
|
+
return self._first_run(
|
|
643
|
+
right,
|
|
644
|
+
mid,
|
|
645
|
+
hi,
|
|
646
|
+
origin + self._total[left],
|
|
647
|
+
minimum,
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _largest_empty_rect(drawable, obstacles, *, target_size=None, warn: bool = True):
|
|
540
652
|
"""Largest axis-aligned empty rectangle in *drawable* avoiding *obstacles*.
|
|
541
653
|
|
|
542
654
|
*drawable* and each obstacle are ``(x0, y0, x1, y1)`` page-mm boxes. Returns
|
|
543
655
|
the empty sub-rectangle of *drawable* (overlapping no obstacle) that maximises
|
|
544
|
-
the
|
|
545
|
-
the
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
maximal empty rectangle has edges drawn from the drawable bounds and
|
|
550
|
-
obstacle bounds
|
|
656
|
+
the uniform scale of *target_size*. The default ``(1, 1)`` maximises the side
|
|
657
|
+
of the largest square it can hold, preserving the iso-view policy. A caller
|
|
658
|
+
placing a wide or tall fixed-aspect footprint can supply its minimum
|
|
659
|
+
``(width, height)`` instead.
|
|
660
|
+
|
|
661
|
+
Every maximal empty rectangle has edges drawn from the drawable bounds and
|
|
662
|
+
the obstacle bounds. Sweep candidate X strips while adding their blocking Y
|
|
663
|
+
intervals to a compressed-coordinate free-run index. This is equivalent to
|
|
664
|
+
enumerating every pair of Y edges, without the quartic blow-up when a detail
|
|
665
|
+
view includes hundreds of decomposed annotation segments (#1065).
|
|
551
666
|
"""
|
|
667
|
+
target_w, target_h = target_size or (1.0, 1.0)
|
|
668
|
+
if not (math.isfinite(target_w) and target_w > 0 and math.isfinite(target_h) and target_h > 0):
|
|
669
|
+
raise ValueError("target_size must contain finite positive dimensions")
|
|
670
|
+
|
|
552
671
|
dx0, dy0, dx1, dy1 = drawable
|
|
553
672
|
xs = sorted({dx0, dx1, *(c for o in obstacles for c in (o[0], o[2]) if dx0 < c < dx1)})
|
|
554
673
|
ys = sorted({dy0, dy1, *(c for o in obstacles for c in (o[1], o[3]) if dy0 < c < dy1)})
|
|
555
674
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
#
|
|
675
|
+
events = []
|
|
676
|
+
for ox0, oy0, ox1, oy1 in obstacles:
|
|
677
|
+
block_lo = max(dy0, oy0)
|
|
678
|
+
block_hi = min(dy1, oy1)
|
|
679
|
+
if block_hi < dy0 or block_lo > dy1:
|
|
680
|
+
continue
|
|
681
|
+
events.append((ox0, ox1, block_lo, block_hi))
|
|
682
|
+
events.sort(key=lambda event: event[0])
|
|
683
|
+
|
|
684
|
+
# The score is min(width / target_w, height / target_h), so any candidate whose
|
|
685
|
+
# width or height cannot exceed the corresponding scaled target cannot beat the
|
|
686
|
+
# best found so far. This is the same exact prune as the square-default path,
|
|
687
|
+
# normalized by the requested footprint.
|
|
566
688
|
best = None
|
|
567
689
|
best_score = 0.0
|
|
568
|
-
nx
|
|
690
|
+
nx = len(xs)
|
|
569
691
|
for i in range(nx - 1):
|
|
570
692
|
rx0 = xs[i]
|
|
571
|
-
if xs[-1] - rx0 <= best_score:
|
|
693
|
+
if xs[-1] - rx0 <= best_score * target_w:
|
|
572
694
|
break # widest strip from here on can't beat best (rx0 only grows)
|
|
573
|
-
|
|
695
|
+
free_y = _FreeRunIndex(ys)
|
|
696
|
+
event_index = 0
|
|
697
|
+
for j in range(bisect_right(xs, rx0 + best_score * target_w), nx):
|
|
574
698
|
rx1 = xs[j]
|
|
575
699
|
width = rx1 - rx0
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
700
|
+
while event_index < len(events) and events[event_index][0] < rx1:
|
|
701
|
+
_, ox1, block_lo, block_hi = events[event_index]
|
|
702
|
+
if rx0 < ox1:
|
|
703
|
+
free_y.block(block_lo, block_hi)
|
|
704
|
+
event_index += 1
|
|
705
|
+
|
|
706
|
+
height = free_y.longest
|
|
707
|
+
score = min(width / target_w, height / target_h)
|
|
708
|
+
if score <= best_score:
|
|
709
|
+
continue
|
|
710
|
+
|
|
711
|
+
# If width caps the score, select the first global Y edge that makes
|
|
712
|
+
# the target fit. Otherwise select the end of the first tallest gap.
|
|
713
|
+
# These are the same deterministic winners as the exhaustive search.
|
|
714
|
+
needed_height = min(width * target_h / target_w, height)
|
|
715
|
+
ry0 = free_y.first_run(needed_height)
|
|
716
|
+
edge = bisect_left(ys, ry0 + needed_height)
|
|
717
|
+
ry1 = ys[min(edge, len(ys) - 1)]
|
|
718
|
+
best_score = score
|
|
719
|
+
best = (rx0, ry0, rx1, ry1)
|
|
591
720
|
if best is None:
|
|
592
721
|
# No empty rectangle exists (obstacles cover the drawable area). This
|
|
593
722
|
# is unreachable in practice — choose_scale always leaves a gap — but
|
|
@@ -819,6 +948,12 @@ class Analysis:
|
|
|
819
948
|
"""
|
|
820
949
|
|
|
821
950
|
part: Shape
|
|
951
|
+
#: The ADR 0017 aggregate, or ``None`` on a DECLARED build — which recognises nothing
|
|
952
|
+
#: (ADR 0011 / #1022). ``None`` means "not detected", never "detected and empty": a
|
|
953
|
+
#: consumer needing an inventory on that path must go through the lazy
|
|
954
|
+
#: ``Drawing._recognition()``, which builds one on demand rather than reading an absence
|
|
955
|
+
#: as an answer.
|
|
956
|
+
recognition: RecognitionResult | None
|
|
822
957
|
bb: BoundBox
|
|
823
958
|
x_size: float
|
|
824
959
|
y_size: float
|
|
@@ -836,7 +971,7 @@ class Analysis:
|
|
|
836
971
|
pads: list # geometry-derived rectangular-pad coverage inventory (#885)
|
|
837
972
|
z_diams: list[float]
|
|
838
973
|
cross_diams: list[float]
|
|
839
|
-
cyls: tuple[
|
|
974
|
+
cyls: tuple[tuple, tuple]
|
|
840
975
|
prof: TurnedProfile | None # turned step profile (recognise_turned_steps), detected once
|
|
841
976
|
od_diam: float | None
|
|
842
977
|
is_rotational: bool
|
|
@@ -12,6 +12,7 @@ drawing grab-bag (ADR 0008; #584 WP2). This module imports nothing from
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
14
|
import logging
|
|
15
|
+
import math
|
|
15
16
|
from dataclasses import dataclass
|
|
16
17
|
|
|
17
18
|
from build123d import Compound
|
|
@@ -108,6 +109,102 @@ def _axis_letter_of(axis) -> str:
|
|
|
108
109
|
return max(zip("xyz", axis, strict=True), key=lambda t: abs(t[1]))[0]
|
|
109
110
|
|
|
110
111
|
|
|
112
|
+
def _axis_direction_components(axis: str, direction=None):
|
|
113
|
+
"""Validate a direction and return ``(raw, norm, dominant_index)``."""
|
|
114
|
+
if axis not in "xyz" or len(axis) != 1:
|
|
115
|
+
raise ValueError("axis must be 'x', 'y', or 'z'")
|
|
116
|
+
if direction is None:
|
|
117
|
+
direction = tuple(1.0 if letter == axis else 0.0 for letter in "xyz")
|
|
118
|
+
try:
|
|
119
|
+
raw = tuple(float(component) for component in direction)
|
|
120
|
+
except (TypeError, ValueError) as exc:
|
|
121
|
+
raise ValueError("axis_direction must be a 3-vector") from exc
|
|
122
|
+
if len(raw) != 3:
|
|
123
|
+
raise ValueError("axis_direction must be a 3-vector")
|
|
124
|
+
if not all(math.isfinite(component) for component in raw):
|
|
125
|
+
raise ValueError("axis_direction must contain only finite values")
|
|
126
|
+
norm = math.hypot(*raw)
|
|
127
|
+
if norm <= 1e-12:
|
|
128
|
+
raise ValueError("axis_direction must be non-zero")
|
|
129
|
+
idx = "xyz".index(axis)
|
|
130
|
+
if abs(raw[idx]) + norm * 1e-9 < max(abs(component) for component in raw):
|
|
131
|
+
raise ValueError(f"axis_direction's dominant component must match axis={axis!r}")
|
|
132
|
+
return raw, norm, idx
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _normalised_axis_direction(axis: str, direction=None) -> tuple[float, float, float]:
|
|
136
|
+
"""Full-precision unit direction used for geometric projection."""
|
|
137
|
+
raw, norm, idx = _axis_direction_components(axis, direction)
|
|
138
|
+
sign = -1.0 if raw[idx] < 0 else 1.0
|
|
139
|
+
return (
|
|
140
|
+
sign * raw[0] / norm,
|
|
141
|
+
sign * raw[1] / norm,
|
|
142
|
+
sign * raw[2] / norm,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _canonical_axis_direction(axis: str, direction=None) -> tuple[float, float, float]:
|
|
147
|
+
"""Return a stable unit direction whose named dominant component is positive.
|
|
148
|
+
|
|
149
|
+
``axis`` remains the orthographic routing hint used by the IR. ``direction`` is the
|
|
150
|
+
actual geometric axis needed to distinguish slanted stock lines; omitting it means the
|
|
151
|
+
corresponding principal axis. Six decimal places preserve direction while keeping
|
|
152
|
+
recognition/declaration identities stable across round trips.
|
|
153
|
+
"""
|
|
154
|
+
raw, norm, idx = _axis_direction_components(axis, direction)
|
|
155
|
+
sign = -1.0 if raw[idx] < 0 else 1.0
|
|
156
|
+
# A previously canonical six-decimal vector is close enough to unit length that
|
|
157
|
+
# normalising it again can move the last digit. Preserve it to make emit -> declare
|
|
158
|
+
# idempotent; arbitrary vectors are still normalised on first entry.
|
|
159
|
+
unit = (
|
|
160
|
+
tuple(sign * component for component in raw)
|
|
161
|
+
if abs(norm - 1.0) <= 2e-6
|
|
162
|
+
else _normalised_axis_direction(axis, raw)
|
|
163
|
+
)
|
|
164
|
+
rounded = tuple(0.0 if abs(component) < 0.5e-6 else round(component, 6) for component in unit)
|
|
165
|
+
return (rounded[0], rounded[1], rounded[2])
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _canonical_axis_span(axis: str, direction, span) -> tuple[float, float]:
|
|
169
|
+
"""Express an axial extent along the canonical positive direction."""
|
|
170
|
+
raw, _norm, idx = _axis_direction_components(axis, direction)
|
|
171
|
+
lo, hi = (float(value) for value in span)
|
|
172
|
+
if raw[idx] < 0:
|
|
173
|
+
lo, hi = -hi, -lo
|
|
174
|
+
return (round(lo, 3), round(hi, 3))
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _axis_line_coordinates(axis: str, point, direction=None) -> tuple[float, float]:
|
|
178
|
+
"""Canonical in-plane coordinates of a 3-D axis line.
|
|
179
|
+
|
|
180
|
+
The perpendicular foot from the origin makes the result invariant to which point on the
|
|
181
|
+
line a geometry kernel reports. The named dominant coordinate is omitted; together with
|
|
182
|
+
the direction it is recoverable from the foot's perpendicularity, so two numbers retain
|
|
183
|
+
the aligned-stock representation while remaining sufficient for slanted stock.
|
|
184
|
+
"""
|
|
185
|
+
px, py, pz = (float(component) for component in point)
|
|
186
|
+
# Use the unrounded unit vector here. Rounding before projection amplifies angular error
|
|
187
|
+
# into millimetres when the reported axis point is tens of metres from the origin.
|
|
188
|
+
vector = _normalised_axis_direction(axis, direction)
|
|
189
|
+
along = px * vector[0] + py * vector[1] + pz * vector[2]
|
|
190
|
+
foot = tuple(component - along * delta for component, delta in zip((px, py, pz), vector))
|
|
191
|
+
keep = [i for i, letter in enumerate("xyz") if letter != axis]
|
|
192
|
+
coordinates = tuple(round(foot[i], 3) for i in keep)
|
|
193
|
+
return (
|
|
194
|
+
0.0 if coordinates[0] == 0 else coordinates[0],
|
|
195
|
+
0.0 if coordinates[1] == 0 else coordinates[1],
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _axis_direction_is_aligned(axis: str, direction, *, tol: float = 1e-3) -> bool:
|
|
200
|
+
"""Whether a canonical direction follows the principal axis named by ``axis``."""
|
|
201
|
+
vector = _canonical_axis_direction(axis, direction)
|
|
202
|
+
idx = "xyz".index(axis)
|
|
203
|
+
return abs(vector[idx] - 1.0) <= tol and all(
|
|
204
|
+
abs(vector[j]) <= tol for j in range(3) if j != idx
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
111
208
|
def _fmt(v: float) -> str:
|
|
112
209
|
"""Format a float as integer string if whole, otherwise 1 dp. The one number
|
|
113
210
|
formatter the IR (:mod:`draftwright.model.ir`) and the drawing layers
|