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