python-cad-tools 0.1.0rc1__py3-none-any.whl
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.
- python_cad_tools/__init__.py +10 -0
- python_cad_tools/_locking.py +63 -0
- python_cad_tools/_project_loader.py +273 -0
- python_cad_tools/_project_state.py +65 -0
- python_cad_tools/_scaffold.py +66 -0
- python_cad_tools/_transaction.py +208 -0
- python_cad_tools/_viewer_static/app.webmanifest +18 -0
- python_cad_tools/_viewer_static/asset-manifest.json +1 -0
- python_cad_tools/_viewer_static/assets/animationGroup-Sd34ntWg.js +1 -0
- python_cad_tools/_viewer_static/assets/bakedVertexAnimation-Cd_pRdrn.js +316 -0
- python_cad_tools/_viewer_static/assets/basisTextureLoader-DWgvS_1G.js +1 -0
- python_cad_tools/_viewer_static/assets/brdfTextureTools-CWGiG1W2.js +2 -0
- python_cad_tools/_viewer_static/assets/bumpFragment-CWULheF1.js +127 -0
- python_cad_tools/_viewer_static/assets/bumpVertex-DqzFZayE.js +6 -0
- python_cad_tools/_viewer_static/assets/bumpVertexDeclaration-C5EFyTio.js +6 -0
- python_cad_tools/_viewer_static/assets/clipPlaneFragment-BXRtt4jj.js +46 -0
- python_cad_tools/_viewer_static/assets/clipPlaneVertex-B_VhD1oQ.js +37 -0
- python_cad_tools/_viewer_static/assets/cubemapToSphericalPolynomial-DRpaukza.js +1 -0
- python_cad_tools/_viewer_static/assets/dds-Dkb2Cx2N.js +1 -0
- python_cad_tools/_viewer_static/assets/dds.pure-DtvJsKvY.js +1 -0
- python_cad_tools/_viewer_static/assets/ddsTextureLoader-H78xux1g.js +1 -0
- python_cad_tools/_viewer_static/assets/default.fragment-oSQ-tWhe.js +469 -0
- python_cad_tools/_viewer_static/assets/default.vertex-DhFjw_Da.js +209 -0
- python_cad_tools/_viewer_static/assets/defaultUboDeclaration-BMDHNppx.js +5 -0
- python_cad_tools/_viewer_static/assets/dumpTools-BaNZPjUX.js +2 -0
- python_cad_tools/_viewer_static/assets/envTextureLoader-V0YnLcIM.js +1 -0
- python_cad_tools/_viewer_static/assets/exrTextureLoader-CjGyIK3x.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphApplyForceBlock-CW5nuobR.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphApplyImpulseBlock-LFqYIkhj.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphArrayIndexBlock-BMOHUbBm.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphBezierCurveEasingBlock-knmTiuEk.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphBinaryOperationBlock-DFLuCAKJ.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphBranchBlock-BJRHMXMo.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphCachedOperationBlock-Dng-6jE0.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphCancelDelayBlock-CgbK3CQZ.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphCodeExecutionBlock-8zMtq9tg.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphConditionalDataBlock-BiLr2rkI.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphConsoleLogBlock-J1vcCtNP.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphConstantBlock-CFb5nuaV.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphContextBlock-DAeGVzhC.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphCounterBlock-lZlAQiJd.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphDataSwitchBlock-BIzvmbVy.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphDebounceBlock-DvNuMVwy.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphDebugBlock-Dymd2VEE.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphDoNBlock-v68aoNLH.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphEasingBlock-CTnGv92q.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphFlipFlopBlock-DXsO0Chw.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphForLoopBlock-CPpe9T75.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphFunctionReferenceBlock-DjDev4Pq.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphGLTFDataProvider-GIYCz4AQ.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphGetAngularVelocityBlock-BStkOVeL.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphGetAssetBlock-Eiqoivbz.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphGetLinearVelocityBlock-B0cpV5ws.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphGetPhysicsMassPropertiesBlock-B-H64yaI.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphGetPropertyBlock-CUyOsPw0.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphGetSoundVolumeBlock-C0FjrsUz.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphGetVariableBlock-O4597zBl.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphIndexOfBlock-E00Z3KXM.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphInterpolationBlock-DnkA9MNH.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphIsKeyPressedBlock-DDfeuAyf.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphIsSoundPlayingBlock-DMXg8jdM.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphJsonPointerParserBlock-OmCorK0y.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphKeyDownEventBlock-RuVtj6g2.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphKeyUpEventBlock-DO4pAfHi.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphKeyboardEventBlock-Bx3pAzzh.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphMathBlocks-7W1XYOC8.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphMathCombineExtractBlocks-a4f0Kv6e.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphMatrixMathBlocks-C5Z-y1Au.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphMeshPickEventBlock-D4m-I4CS.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphMultiGateBlock-BISVSMw6.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphPauseAnimationBlock-BYP2Z-HG.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphPauseSoundBlock-Drh1EeW6.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphPhysicsCollisionEventBlock-BoziAQ75.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphPlayAnimationBlock-Clry-68M.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphPlaySoundBlock-LbBVxGBE.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphPointerDownEventBlock-DIVXCSHh.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphPointerMoveEventBlock-BNNOzY2W.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphPointerOutEventBlock-B8f2dOFT.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphPointerOverEventBlock-CQr8LCee.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphPointerUpEventBlock-DMg_iEHl.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphReceiveCustomEventBlock-B8GJm9Ai.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphSceneReadyEventBlock-Dcdb29K2.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphSceneTickEventBlock-BLE5p4mi.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphSendCustomEventBlock-BMzDItQ5.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphSequenceBlock-DRURovjn.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphSetAngularVelocityBlock-Ba8dhkLM.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphSetDelayBlock-CnQR--Gn.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphSetLinearVelocityBlock-Dm1r3vkh.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphSetPhysicsMotionTypeBlock-BNtMLimi.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphSetPropertyBlock-C-uv2jhP.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphSetSoundVolumeBlock-D9LDx3lg.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphSetVariableBlock-DszzRB5X.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphSoundEndedEventBlock-DeTDSudb.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphStopAnimationBlock-DWuwoFWr.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphStopSoundBlock-DEnj2cQw.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphSwitchBlock-zs8Fp9ky.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphThrottleBlock-BYAKUQXd.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphTransformCoordinatesSystemBlock-DYP54m5P.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphTypeToTypeBlocks-vFwn79bS.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphUnaryOperationBlock-CaRh4Ef0.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphVectorMathBlocks-BEIhMs-A.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphWaitAllBlock-DFbELP_j.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphWhileLoopBlock-C1OKQ-_F.js +1 -0
- python_cad_tools/_viewer_static/assets/fogFragment-DtDlQBnz.js +25 -0
- python_cad_tools/_viewer_static/assets/gaussianSplatting-Cfigjb15.js +242 -0
- python_cad_tools/_viewer_static/assets/gaussianSplatting-DIn8R8oF.js +296 -0
- python_cad_tools/_viewer_static/assets/gaussianSplatting.fragment-BxeRJ2k-.js +38 -0
- python_cad_tools/_viewer_static/assets/gaussianSplatting.fragment-DNDmvRs-.js +37 -0
- python_cad_tools/_viewer_static/assets/gaussianSplatting.vertex-D92z5jYn.js +86 -0
- python_cad_tools/_viewer_static/assets/gaussianSplatting.vertex-DSWfFnKQ.js +86 -0
- python_cad_tools/_viewer_static/assets/gaussianSplattingVoxel.fragment-DBQE2gDk.js +12 -0
- python_cad_tools/_viewer_static/assets/gaussianSplattingVoxel.fragment-qwx5ntqJ.js +17 -0
- python_cad_tools/_viewer_static/assets/gaussianSplattingVoxel.vertex-CP2kd3jU.js +30 -0
- python_cad_tools/_viewer_static/assets/gaussianSplattingVoxel.vertex-Dr-fEg_W.js +19 -0
- python_cad_tools/_viewer_static/assets/geometry.fragment-CsJIm2H0.js +242 -0
- python_cad_tools/_viewer_static/assets/geometry.fragment-Dph3muZt.js +262 -0
- python_cad_tools/_viewer_static/assets/geometry.vertex-BXGMTA_J.js +212 -0
- python_cad_tools/_viewer_static/assets/geometry.vertex-Bm9Wg3Gi.js +214 -0
- python_cad_tools/_viewer_static/assets/glowBlurPostProcess.fragment-BJSwKIJD.js +7 -0
- python_cad_tools/_viewer_static/assets/glowMapGeneration.fragment-DcyS1cTF.js +63 -0
- python_cad_tools/_viewer_static/assets/glowMapGeneration.vertex-BGq1Fakj.js +75 -0
- python_cad_tools/_viewer_static/assets/glowMapMerge.fragment-DxcneU_F.js +25 -0
- python_cad_tools/_viewer_static/assets/glowMapMerge.vertex-DaJJKqMV.js +8 -0
- python_cad_tools/_viewer_static/assets/harmonicsFunctions-DzJcvNWG.js +16 -0
- python_cad_tools/_viewer_static/assets/hdrTextureLoader-BGQvgh0h.js +2 -0
- python_cad_tools/_viewer_static/assets/helperFunctions-Cugfp1Fz.js +87 -0
- python_cad_tools/_viewer_static/assets/iesTextureLoader-CO4G0p2M.js +2 -0
- python_cad_tools/_viewer_static/assets/index-B2zBEWsq.js +7007 -0
- python_cad_tools/_viewer_static/assets/index-DcTTrc-8.css +1 -0
- python_cad_tools/_viewer_static/assets/kernelBlur.fragment-B6CTXTxY.js +57 -0
- python_cad_tools/_viewer_static/assets/kernelBlur.fragment-BkKbudbW.js +57 -0
- python_cad_tools/_viewer_static/assets/kernelBlur.vertex-BYwFU5IS.js +11 -0
- python_cad_tools/_viewer_static/assets/kernelBlur.vertex-D371zUfT.js +11 -0
- python_cad_tools/_viewer_static/assets/kernelBlurVaryingDeclaration-CgBGaCHg.js +1 -0
- python_cad_tools/_viewer_static/assets/kernelBlurVaryingDeclaration-YS8JpqdO.js +1 -0
- python_cad_tools/_viewer_static/assets/ktxTextureLoader-ncMSJTjl.js +1 -0
- python_cad_tools/_viewer_static/assets/lightFragment-skHiFbKB.js +398 -0
- python_cad_tools/_viewer_static/assets/logDepthDeclaration-BR8XRz9F.js +4 -0
- python_cad_tools/_viewer_static/assets/logDepthVertex-UKrhEIvg.js +14 -0
- python_cad_tools/_viewer_static/assets/mainUVVaryingDeclaration-BxQWGFcG.js +4 -0
- python_cad_tools/_viewer_static/assets/mesh.vertexData.functions-ayNlGC5K.js +1 -0
- python_cad_tools/_viewer_static/assets/meshUboDeclaration-BnX046aQ.js +4 -0
- python_cad_tools/_viewer_static/assets/oitFragment-CTMCOfNP.js +605 -0
- python_cad_tools/_viewer_static/assets/openpbr.fragment-DPJPqi6p.js +1530 -0
- python_cad_tools/_viewer_static/assets/openpbr.fragment-Dv6qTVVi.js +1384 -0
- python_cad_tools/_viewer_static/assets/openpbr.vertex-Bn2wrdGL.js +364 -0
- python_cad_tools/_viewer_static/assets/openpbr.vertex-CHESW3gB.js +251 -0
- python_cad_tools/_viewer_static/assets/openpbrMaterial.pure-BdK7VgO_.js +3 -0
- python_cad_tools/_viewer_static/assets/openpbrMaterialLoadingAdapter-Bm_AHDUN.js +4 -0
- python_cad_tools/_viewer_static/assets/openpbrTransmissionLayerData-BDa9qEoN.js +339 -0
- python_cad_tools/_viewer_static/assets/openpbrTransmissionLayerData-Ba_ly4LE.js +343 -0
- python_cad_tools/_viewer_static/assets/openpbrUboDeclaration-BY2lSH2C.js +5 -0
- python_cad_tools/_viewer_static/assets/openpbrUboDeclaration-DajhikDS.js +6 -0
- python_cad_tools/_viewer_static/assets/packingFunctions-BHPwcGG2.js +4 -0
- python_cad_tools/_viewer_static/assets/packingFunctions-DTwzBRBx.js +4 -0
- python_cad_tools/_viewer_static/assets/pass.fragment-9oKU4pI3.js +4 -0
- python_cad_tools/_viewer_static/assets/passCube.fragment-CpAeiYx1.js +23 -0
- python_cad_tools/_viewer_static/assets/passCube.fragment-DHtSWVRF.js +23 -0
- python_cad_tools/_viewer_static/assets/pbr.fragment-cIhMKwLs.js +2560 -0
- python_cad_tools/_viewer_static/assets/pbr.vertex-BfdONsf2.js +255 -0
- python_cad_tools/_viewer_static/assets/pbrBRDFFunctions-B_ppOzPO.js +173 -0
- python_cad_tools/_viewer_static/assets/pbrDebug-DDdwL5_t.js +693 -0
- python_cad_tools/_viewer_static/assets/pbrIBLFunctions-b1zdpzOP.js +43 -0
- python_cad_tools/_viewer_static/assets/pbrMaterial.pure-BZA7yVhu.js +2 -0
- python_cad_tools/_viewer_static/assets/pbrMaterialLoadingAdapter-BRvKRnjl.js +1 -0
- python_cad_tools/_viewer_static/assets/pbrUboDeclaration-CMZp25k3.js +5 -0
- python_cad_tools/_viewer_static/assets/postprocess.vertex-DzdUpNbW.js +9 -0
- python_cad_tools/_viewer_static/assets/procedural.vertex-BnbuQ-1f.js +7 -0
- python_cad_tools/_viewer_static/assets/procedural.vertex-Dp-Ng7Pc.js +8 -0
- python_cad_tools/_viewer_static/assets/reflectionFunction-DBlr3oDs.js +83 -0
- python_cad_tools/_viewer_static/assets/rgbdDecode.fragment-CyvnZFOJ.js +5 -0
- python_cad_tools/_viewer_static/assets/rgbdEncode.fragment-Dz4zLnoD.js +5 -0
- python_cad_tools/_viewer_static/assets/rgbdEncode.fragment-iscgpA1_.js +5 -0
- python_cad_tools/_viewer_static/assets/sceneUboDeclaration-BzFIc4mb.js +11 -0
- python_cad_tools/_viewer_static/assets/textureProcessor.fragment-C9D0rd-M.js +151 -0
- python_cad_tools/_viewer_static/assets/textureProcessor.fragment-JMKyWZng.js +146 -0
- python_cad_tools/_viewer_static/assets/tgaTextureLoader-oxTElIQJ.js +1 -0
- python_cad_tools/_viewer_static/assets/vertexColorMixing-mdC9xZe1.js +418 -0
- python_cad_tools/_viewer_static/favicon.svg +10 -0
- python_cad_tools/_viewer_static/index.html +18 -0
- python_cad_tools/_viewer_static/sw.js +61 -0
- python_cad_tools/artifacts.py +546 -0
- python_cad_tools/build-info.json +1 -0
- python_cad_tools/build.py +270 -0
- python_cad_tools/cli.py +212 -0
- python_cad_tools/context.py +67 -0
- python_cad_tools/determinism.py +66 -0
- python_cad_tools/drawings/__init__.py +124 -0
- python_cad_tools/drawings/_catalog.py +79 -0
- python_cad_tools/drawings/_validation.py +238 -0
- python_cad_tools/elements.py +121 -0
- python_cad_tools/exceptions.py +45 -0
- python_cad_tools/exporters/__init__.py +8 -0
- python_cad_tools/exporters/base.py +24 -0
- python_cad_tools/exporters/drawings/__init__.py +5 -0
- python_cad_tools/exporters/drawings/annotations.py +444 -0
- python_cad_tools/exporters/drawings/dxf.py +110 -0
- python_cad_tools/exporters/drawings/model.py +70 -0
- python_cad_tools/exporters/drawings/pdf.py +119 -0
- python_cad_tools/exporters/drawings/primitives.py +93 -0
- python_cad_tools/exporters/drawings/projection.py +64 -0
- python_cad_tools/exporters/drawings/svg.py +135 -0
- python_cad_tools/exporters/glb.py +100 -0
- python_cad_tools/exporters/ifc.py +229 -0
- python_cad_tools/exporters/quantities.py +136 -0
- python_cad_tools/exporters/step.py +117 -0
- python_cad_tools/geometry.py +187 -0
- python_cad_tools/manifests.py +338 -0
- python_cad_tools/project_config.py +174 -0
- python_cad_tools/py.typed +1 -0
- python_cad_tools/runtime_sync.py +126 -0
- python_cad_tools/schemas/build-manifest-1.json +1 -0
- python_cad_tools/schemas/design-manifest-1.json +1 -0
- python_cad_tools/schemas/download-manifest-2.json +46 -0
- python_cad_tools/schemas/drawing-annotations-1.json +66 -0
- python_cad_tools/schemas/glb-manifest-1.json +1 -0
- python_cad_tools/schemas/ifc-validation-1.json +1 -0
- python_cad_tools/schemas/quantities-1.json +1 -0
- python_cad_tools/schemas/run-metadata-1.json +1 -0
- python_cad_tools/schemas/site-metadata-1.json +34 -0
- python_cad_tools/schemas/step-validation-1.json +1 -0
- python_cad_tools/site.py +676 -0
- python_cad_tools/templates/project/.gitignore +9 -0
- python_cad_tools/templates/project/README.md +38 -0
- python_cad_tools/templates/project/config.py +36 -0
- python_cad_tools/templates/project/drawing_annotations.py +117 -0
- python_cad_tools/templates/project/model.py +87 -0
- python_cad_tools/templates/project/pyproject.toml +27 -0
- python_cad_tools/units.py +207 -0
- python_cad_tools/validation.py +153 -0
- python_cad_tools-0.1.0rc1.dist-info/METADATA +149 -0
- python_cad_tools-0.1.0rc1.dist-info/RECORD +236 -0
- python_cad_tools-0.1.0rc1.dist-info/WHEEL +5 -0
- python_cad_tools-0.1.0rc1.dist-info/entry_points.txt +2 -0
- python_cad_tools-0.1.0rc1.dist-info/licenses/LICENSE +679 -0
- python_cad_tools-0.1.0rc1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Python CAD Project
|
|
2
|
+
|
|
3
|
+
This project uses `model.py`, `config.py`, and `drawing_annotations.py` as its authoritative design source. One headless build generates exact STEP, IFC4, GLB, conceptual SVG/DXF/PDF drawings, quantities, validation reports, and manifests.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
python3 -m venv .venv
|
|
9
|
+
.venv/bin/pip install python-cad-tools
|
|
10
|
+
python-cad build
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
On Windows, use the executables under `.venv\Scripts`.
|
|
14
|
+
|
|
15
|
+
Edit project dimensions and materials in `config.py` and build shared elements in `model.py`. Preserve stable semantic IDs. Do not edit files under `generated/`.
|
|
16
|
+
|
|
17
|
+
Useful commands:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
python-cad build
|
|
21
|
+
python-cad build --format step
|
|
22
|
+
python-cad build --format ifc --format quantities
|
|
23
|
+
python-cad validate
|
|
24
|
+
python-cad verify
|
|
25
|
+
python-cad clean
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`generated/` is ignored by Git except for `.gitkeep`. CI should upload generated artifacts instead of committing them.
|
|
29
|
+
|
|
30
|
+
## Review in the browser
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
python-cad serve
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This builds the latest CAD artifacts and serves a static, read-only viewer site on the loopback address. See `python-cad serve --help` for host, port, and base-path options.
|
|
37
|
+
|
|
38
|
+
Conceptual drawings are not for construction or permitting. This project does not provide engineering, code, permit, survey, or licensed-trade approval.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Project-owned typed dimensions, materials, colors, and export settings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from python_cad_tools.units import FOOT, INCH, MM, Length
|
|
8
|
+
|
|
9
|
+
PROJECT_NAME = "ProjectModel"
|
|
10
|
+
BASE_LENGTH = 10 * FOOT
|
|
11
|
+
BASE_WIDTH = 6 * FOOT
|
|
12
|
+
BASE_THICKNESS = 6 * INCH
|
|
13
|
+
BLOCK_LENGTH = 4 * FOOT
|
|
14
|
+
BLOCK_WIDTH = 3 * FOOT
|
|
15
|
+
BLOCK_HEIGHT = 2 * FOOT
|
|
16
|
+
BASE_COLOR = (0.65, 0.65, 0.62)
|
|
17
|
+
PRIMARY_COLOR = (0.25, 0.42, 0.58)
|
|
18
|
+
ACCENT_COLOR = (0.12, 0.12, 0.12)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class ExportSettings:
|
|
23
|
+
step_schema_preference: str = "AP242"
|
|
24
|
+
glb_linear_deflection_mm: float = 0.5
|
|
25
|
+
glb_angular_deflection_rad: float = 0.25
|
|
26
|
+
include_reference_geometry_in_glb: bool = False
|
|
27
|
+
drawing_dimension_denominator: int = 16
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
EXPORTS = ExportSettings()
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"ACCENT_COLOR", "BASE_COLOR", "BASE_LENGTH", "BASE_THICKNESS", "BASE_WIDTH",
|
|
34
|
+
"BLOCK_HEIGHT", "BLOCK_LENGTH", "BLOCK_WIDTH", "EXPORTS", "FOOT", "INCH",
|
|
35
|
+
"Length", "MM", "PRIMARY_COLOR", "PROJECT_NAME",
|
|
36
|
+
]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Project-owned drawing annotation intent derived from the shared model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from python_cad_tools.context import DrawingContext
|
|
6
|
+
from python_cad_tools.drawings import (
|
|
7
|
+
DrawingAnnotationSet,
|
|
8
|
+
ElevationMarker,
|
|
9
|
+
Line,
|
|
10
|
+
Polyline,
|
|
11
|
+
SectionCallout,
|
|
12
|
+
SheetAnnotations,
|
|
13
|
+
Table,
|
|
14
|
+
TableRow,
|
|
15
|
+
Text,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def build_annotations(context: DrawingContext) -> DrawingAnnotationSet:
|
|
20
|
+
"""Return renderer-independent annotations derived from the shared model."""
|
|
21
|
+
plan = next(sheet for sheet in context.sheets if sheet.sheet_id == "A-101")
|
|
22
|
+
min_x, min_y, max_x, max_y = plan.projected_bounds_mm
|
|
23
|
+
center_x = (min_x + max_x) / 2
|
|
24
|
+
center_y = (min_y + max_y) / 2
|
|
25
|
+
physical = [element for element in context.model.walk() if element.physical]
|
|
26
|
+
rows = tuple(
|
|
27
|
+
TableRow(
|
|
28
|
+
id=f"item-{index}",
|
|
29
|
+
cells=(
|
|
30
|
+
element.id,
|
|
31
|
+
_dimension(element.dimensions.length_mm),
|
|
32
|
+
_dimension(element.dimensions.width_mm),
|
|
33
|
+
_dimension(element.dimensions.height_mm),
|
|
34
|
+
),
|
|
35
|
+
)
|
|
36
|
+
for index, element in enumerate(physical, start=1)
|
|
37
|
+
)
|
|
38
|
+
return DrawingAnnotationSet(
|
|
39
|
+
provider_id="generic.annotations.v1",
|
|
40
|
+
sheets=(
|
|
41
|
+
SheetAnnotations(
|
|
42
|
+
sheet_id="A-101",
|
|
43
|
+
annotations=(
|
|
44
|
+
SectionCallout(
|
|
45
|
+
id="generic.section.main",
|
|
46
|
+
sheet_id="A-101",
|
|
47
|
+
start=(center_x, min_y),
|
|
48
|
+
end=(center_x, max_y),
|
|
49
|
+
reference_sheet_id="A-301",
|
|
50
|
+
label="Section A",
|
|
51
|
+
view_direction="right",
|
|
52
|
+
),
|
|
53
|
+
ElevationMarker(
|
|
54
|
+
id="generic.elevation.front",
|
|
55
|
+
sheet_id="A-101",
|
|
56
|
+
position=(min_x + (max_x - min_x) * 0.2, center_y),
|
|
57
|
+
reference_sheet_id="A-201",
|
|
58
|
+
label="Front Elevation",
|
|
59
|
+
direction="down",
|
|
60
|
+
),
|
|
61
|
+
Table(
|
|
62
|
+
id="generic.schedule.elements",
|
|
63
|
+
sheet_id="A-101",
|
|
64
|
+
position=(max_x + 120.0, center_y),
|
|
65
|
+
title="Element Schedule",
|
|
66
|
+
columns=("Stable ID", "Length mm", "Width mm", "Height mm"),
|
|
67
|
+
rows=rows,
|
|
68
|
+
),
|
|
69
|
+
),
|
|
70
|
+
),
|
|
71
|
+
SheetAnnotations(
|
|
72
|
+
sheet_id="A-201",
|
|
73
|
+
annotations=(
|
|
74
|
+
Line(
|
|
75
|
+
id="generic.datum.front",
|
|
76
|
+
sheet_id="A-201",
|
|
77
|
+
start=(min_x, 0.0),
|
|
78
|
+
end=(max_x, 0.0),
|
|
79
|
+
style="annotation-line",
|
|
80
|
+
),
|
|
81
|
+
),
|
|
82
|
+
),
|
|
83
|
+
SheetAnnotations(
|
|
84
|
+
sheet_id="A-202",
|
|
85
|
+
annotations=(
|
|
86
|
+
Polyline(
|
|
87
|
+
id="generic.boundary.side",
|
|
88
|
+
sheet_id="A-202",
|
|
89
|
+
points=((min_y, 0.0), (max_y, 0.0), (max_y, 80.0)),
|
|
90
|
+
closed=False,
|
|
91
|
+
style="annotation-line",
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
),
|
|
95
|
+
SheetAnnotations(
|
|
96
|
+
sheet_id="A-301",
|
|
97
|
+
annotations=(
|
|
98
|
+
Text(
|
|
99
|
+
id="generic.note.section",
|
|
100
|
+
sheet_id="A-301",
|
|
101
|
+
position=(min_x, 100.0),
|
|
102
|
+
value=f"Model: {context.model.name}",
|
|
103
|
+
height_mm=12.0,
|
|
104
|
+
rotation_degrees=0.0,
|
|
105
|
+
style="annotation-text",
|
|
106
|
+
),
|
|
107
|
+
),
|
|
108
|
+
),
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
__all__ = ["build_annotations"]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _dimension(value: float | None) -> str:
|
|
117
|
+
return "not specified" if value is None else f"{value:g}"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Project-owned starter design source; this builds the shared model exactly once."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import config as cfg
|
|
6
|
+
|
|
7
|
+
from python_cad_tools.context import BuildContext
|
|
8
|
+
from python_cad_tools.elements import (
|
|
9
|
+
DesignElement,
|
|
10
|
+
DesignModel,
|
|
11
|
+
Dimensions,
|
|
12
|
+
IfcMapping,
|
|
13
|
+
MaterialSpec,
|
|
14
|
+
Placement,
|
|
15
|
+
)
|
|
16
|
+
from python_cad_tools.geometry import bounds, box, reference_line
|
|
17
|
+
from python_cad_tools.units import INCH, to_mm
|
|
18
|
+
from python_cad_tools.validation import ValidationIssue
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_model(context: BuildContext) -> DesignModel:
|
|
22
|
+
base_material = MaterialSpec("material.starter.base", "Starter Base Material", "example", cfg.BASE_COLOR)
|
|
23
|
+
primary_material = MaterialSpec("material.starter.primary", "Starter Primary Material", "example", cfg.PRIMARY_COLOR)
|
|
24
|
+
block_x = (cfg.BASE_LENGTH - cfg.BLOCK_LENGTH) / 2
|
|
25
|
+
block_y = (cfg.BASE_WIDTH - cfg.BLOCK_WIDTH) / 2
|
|
26
|
+
block_z = cfg.BASE_THICKNESS
|
|
27
|
+
axis_x_z = cfg.BASE_THICKNESS + cfg.BLOCK_HEIGHT + 6 * INCH
|
|
28
|
+
axis_y_z = cfg.BASE_THICKNESS + cfg.BLOCK_HEIGHT + 10 * INCH
|
|
29
|
+
zero = 0 * cfg.MM
|
|
30
|
+
elements = [
|
|
31
|
+
DesignElement(
|
|
32
|
+
id="starter.base_plate", name="Base Plate", category="physical-example",
|
|
33
|
+
geometry=box(cfg.BASE_LENGTH, cfg.BASE_WIDTH, cfg.BASE_THICKNESS, origin=(zero, zero, zero)),
|
|
34
|
+
geometry_kind="solid", dimensions=Dimensions(to_mm(cfg.BASE_LENGTH), to_mm(cfg.BASE_WIDTH), to_mm(cfg.BASE_THICKNESS)),
|
|
35
|
+
material=base_material, color_rgb=cfg.BASE_COLOR,
|
|
36
|
+
ifc_mapping=IfcMapping("IfcBuildingElementProxy", "NOTDEFINED"), storey="Default Storey",
|
|
37
|
+
tags={"starter", "physical"}, properties={"quantity_provenance": "exact_geometry"},
|
|
38
|
+
),
|
|
39
|
+
DesignElement(
|
|
40
|
+
id="starter.primary_block", name="Primary Block", category="physical-example",
|
|
41
|
+
geometry=box(cfg.BLOCK_LENGTH, cfg.BLOCK_WIDTH, cfg.BLOCK_HEIGHT, origin=(block_x, block_y, block_z)),
|
|
42
|
+
geometry_kind="solid", placement=Placement((to_mm(block_x), to_mm(block_y), to_mm(block_z))),
|
|
43
|
+
dimensions=Dimensions(to_mm(cfg.BLOCK_LENGTH), to_mm(cfg.BLOCK_WIDTH), to_mm(cfg.BLOCK_HEIGHT)),
|
|
44
|
+
material=primary_material, color_rgb=cfg.PRIMARY_COLOR,
|
|
45
|
+
ifc_mapping=IfcMapping("IfcBuildingElementProxy", "NOTDEFINED"), storey="Default Storey",
|
|
46
|
+
tags={"starter", "physical"}, properties={"quantity_provenance": "exact_geometry"},
|
|
47
|
+
),
|
|
48
|
+
DesignElement(
|
|
49
|
+
id="reference.axis_x", name="X Reference Axis", category="reference-axis",
|
|
50
|
+
geometry=reference_line((zero, cfg.BASE_WIDTH / 2, axis_x_z), (cfg.BASE_LENGTH, cfg.BASE_WIDTH / 2, axis_x_z)),
|
|
51
|
+
geometry_kind="reference", placement=Placement((0.0, to_mm(cfg.BASE_WIDTH / 2), to_mm(axis_x_z))),
|
|
52
|
+
dimensions=Dimensions(length_mm=to_mm(cfg.BASE_LENGTH), radius_mm=to_mm(0.75 * INCH)),
|
|
53
|
+
color_rgb=cfg.ACCENT_COLOR, ifc_mapping=IfcMapping(None, exclude_reason="non-physical reference geometry"),
|
|
54
|
+
tags={"starter", "reference", "axis-x"}, export_formats={"drawings"}, physical=False,
|
|
55
|
+
),
|
|
56
|
+
DesignElement(
|
|
57
|
+
id="reference.axis_y", name="Y Reference Axis", category="reference-axis",
|
|
58
|
+
geometry=reference_line((cfg.BASE_LENGTH / 2, zero, axis_y_z), (cfg.BASE_LENGTH / 2, cfg.BASE_WIDTH, axis_y_z)),
|
|
59
|
+
geometry_kind="reference", placement=Placement((to_mm(cfg.BASE_LENGTH / 2), 0.0, to_mm(axis_y_z))),
|
|
60
|
+
dimensions=Dimensions(length_mm=to_mm(cfg.BASE_WIDTH), radius_mm=to_mm(0.75 * INCH)),
|
|
61
|
+
color_rgb=cfg.ACCENT_COLOR, ifc_mapping=IfcMapping(None, exclude_reason="non-physical reference geometry"),
|
|
62
|
+
tags={"starter", "reference", "axis-y"}, export_formats={"drawings"}, physical=False,
|
|
63
|
+
),
|
|
64
|
+
]
|
|
65
|
+
return DesignModel(
|
|
66
|
+
id="starter.project_model", name=cfg.PROJECT_NAME, artifact_stem="starter_project", elements=elements,
|
|
67
|
+
metadata={"fixture": "generic starter scaffold", "authoritative_source": "model.py"}, validators=[_validate_starter],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _validate_starter(model: DesignModel) -> list[ValidationIssue]:
|
|
72
|
+
base = model.get("starter.base_plate")
|
|
73
|
+
block = model.get("starter.primary_block")
|
|
74
|
+
base_bounds = bounds(base.geometry)
|
|
75
|
+
block_bounds = bounds(block.geometry)
|
|
76
|
+
checks = {
|
|
77
|
+
"block-centered-x": abs(block_bounds[0] - (to_mm(cfg.BASE_LENGTH) - to_mm(cfg.BLOCK_LENGTH)) / 2) <= 1e-6,
|
|
78
|
+
"block-centered-y": abs(block_bounds[1] - (to_mm(cfg.BASE_WIDTH) - to_mm(cfg.BLOCK_WIDTH)) / 2) <= 1e-6,
|
|
79
|
+
"block-rests-on-base": abs(block_bounds[2] - base_bounds[5]) <= 1e-6,
|
|
80
|
+
"base-color": base.color_rgb == cfg.BASE_COLOR,
|
|
81
|
+
"block-color": block.color_rgb == cfg.PRIMARY_COLOR,
|
|
82
|
+
"reference-not-physical": all(not model.get(stable_id).physical for stable_id in ("reference.axis_x", "reference.axis_y")),
|
|
83
|
+
}
|
|
84
|
+
return [
|
|
85
|
+
ValidationIssue(f"starter-{code}", "error", f"Starter invariant failed: {code}")
|
|
86
|
+
for code, passed in checks.items() if not passed
|
|
87
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "python-cad-design"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
requires-python = ">=3.12,<3.14"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"python-cad-tools==0.1.0rc1",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[tool.python-cad]
|
|
14
|
+
schema-version = 1
|
|
15
|
+
config = "config"
|
|
16
|
+
model = "model:build_model"
|
|
17
|
+
drawing-annotations = "drawing_annotations:build_annotations"
|
|
18
|
+
source-inputs = ["pyproject.toml", "config.py", "model.py", "drawing_annotations.py"]
|
|
19
|
+
output-directory = "generated"
|
|
20
|
+
default-formats = ["step", "ifc", "glb", "drawings", "quantities"]
|
|
21
|
+
|
|
22
|
+
[tool.mypy]
|
|
23
|
+
python_version = "3.12"
|
|
24
|
+
files = ["model.py", "config.py"]
|
|
25
|
+
ignore_missing_imports = true
|
|
26
|
+
check_untyped_defs = true
|
|
27
|
+
warn_unused_ignores = true
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Dimension-safe unit types; the geometry kernel boundary is millimetres."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from math import isfinite
|
|
7
|
+
from typing import overload
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True, order=True)
|
|
11
|
+
class Length:
|
|
12
|
+
mm: float
|
|
13
|
+
|
|
14
|
+
def __post_init__(self) -> None:
|
|
15
|
+
if not isinstance(self.mm, (int, float)) or not isfinite(self.mm):
|
|
16
|
+
raise ValueError("Length requires a finite numeric millimetre value")
|
|
17
|
+
|
|
18
|
+
@overload
|
|
19
|
+
def __mul__(self, other: float) -> Length: ...
|
|
20
|
+
|
|
21
|
+
@overload
|
|
22
|
+
def __mul__(self, other: Length) -> Area: ...
|
|
23
|
+
|
|
24
|
+
def __mul__(self, other: float | Length) -> Length | Area:
|
|
25
|
+
if isinstance(other, Length):
|
|
26
|
+
return Area(self.mm * other.mm)
|
|
27
|
+
if isinstance(other, (int, float)):
|
|
28
|
+
return Length(self.mm * other)
|
|
29
|
+
return NotImplemented
|
|
30
|
+
|
|
31
|
+
def __rmul__(self, other: float) -> Length:
|
|
32
|
+
return Length(self.mm * other)
|
|
33
|
+
|
|
34
|
+
@overload
|
|
35
|
+
def __truediv__(self, other: float) -> Length: ...
|
|
36
|
+
|
|
37
|
+
@overload
|
|
38
|
+
def __truediv__(self, other: Length) -> float: ...
|
|
39
|
+
|
|
40
|
+
def __truediv__(self, other: float | Length) -> Length | float:
|
|
41
|
+
if isinstance(other, Length):
|
|
42
|
+
return self.mm / other.mm
|
|
43
|
+
if isinstance(other, (int, float)):
|
|
44
|
+
return Length(self.mm / other)
|
|
45
|
+
return NotImplemented
|
|
46
|
+
|
|
47
|
+
def __add__(self, other: Length) -> Length:
|
|
48
|
+
if not isinstance(other, Length):
|
|
49
|
+
return NotImplemented
|
|
50
|
+
return Length(self.mm + other.mm)
|
|
51
|
+
|
|
52
|
+
def __sub__(self, other: Length) -> Length:
|
|
53
|
+
if not isinstance(other, Length):
|
|
54
|
+
return NotImplemented
|
|
55
|
+
return Length(self.mm - other.mm)
|
|
56
|
+
|
|
57
|
+
def __neg__(self) -> Length:
|
|
58
|
+
return Length(-self.mm)
|
|
59
|
+
|
|
60
|
+
def __abs__(self) -> Length:
|
|
61
|
+
return Length(abs(self.mm))
|
|
62
|
+
|
|
63
|
+
def __float__(self) -> float:
|
|
64
|
+
raise TypeError("Use to_mm(length) for an explicit kernel conversion")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True, slots=True)
|
|
68
|
+
class Area:
|
|
69
|
+
mm2: float
|
|
70
|
+
|
|
71
|
+
def __post_init__(self) -> None:
|
|
72
|
+
if not isinstance(self.mm2, (int, float)) or not isfinite(self.mm2):
|
|
73
|
+
raise ValueError("Area requires a finite numeric square-millimetre value")
|
|
74
|
+
|
|
75
|
+
@overload
|
|
76
|
+
def __mul__(self, other: float) -> Area: ...
|
|
77
|
+
|
|
78
|
+
@overload
|
|
79
|
+
def __mul__(self, other: Length) -> Volume: ...
|
|
80
|
+
|
|
81
|
+
def __mul__(self, other: float | Length) -> Area | Volume:
|
|
82
|
+
if isinstance(other, Length):
|
|
83
|
+
return Volume(self.mm2 * other.mm)
|
|
84
|
+
if isinstance(other, (int, float)):
|
|
85
|
+
return Area(self.mm2 * other)
|
|
86
|
+
return NotImplemented
|
|
87
|
+
|
|
88
|
+
def __rmul__(self, other: float) -> Area:
|
|
89
|
+
return Area(self.mm2 * other)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(frozen=True, slots=True)
|
|
93
|
+
class Volume:
|
|
94
|
+
mm3: float
|
|
95
|
+
|
|
96
|
+
def __post_init__(self) -> None:
|
|
97
|
+
if not isinstance(self.mm3, (int, float)) or not isfinite(self.mm3):
|
|
98
|
+
raise ValueError("Volume requires a finite numeric cubic-millimetre value")
|
|
99
|
+
|
|
100
|
+
def __mul__(self, other: float) -> Volume:
|
|
101
|
+
if not isinstance(other, (int, float)):
|
|
102
|
+
return NotImplemented
|
|
103
|
+
return Volume(self.mm3 * other)
|
|
104
|
+
|
|
105
|
+
__rmul__ = __mul__
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
MM = Length(1.0)
|
|
109
|
+
INCH = Length(25.4)
|
|
110
|
+
FOOT = 12 * INCH
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def to_mm(value: Length) -> float:
|
|
114
|
+
if not isinstance(value, Length):
|
|
115
|
+
raise TypeError(f"Expected Length, got {type(value).__name__}")
|
|
116
|
+
return float(value.mm)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def mm(value: float) -> Length:
|
|
120
|
+
return value * MM
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def inches(value: float) -> Length:
|
|
124
|
+
return value * INCH
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def feet(value: float) -> Length:
|
|
128
|
+
return value * FOOT
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def feet_inches(feet_value: float, inches_value: float = 0) -> Length:
|
|
132
|
+
return feet_value * FOOT + inches_value * INCH
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def square_feet(value: Area) -> float:
|
|
136
|
+
if not isinstance(value, Area):
|
|
137
|
+
raise TypeError("square_feet expects Area")
|
|
138
|
+
return value.mm2 / (304.8**2)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def cubic_feet(value: Volume) -> float:
|
|
142
|
+
if not isinstance(value, Volume):
|
|
143
|
+
raise TypeError("cubic_feet expects Volume")
|
|
144
|
+
return value.mm3 / (304.8**3)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def cubic_yards(value: Volume) -> float:
|
|
148
|
+
if not isinstance(value, Volume):
|
|
149
|
+
raise TypeError("cubic_yards expects Volume")
|
|
150
|
+
return value.mm3 / (914.4**3)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def metres(value: Length) -> float:
|
|
154
|
+
return to_mm(value) / 1000.0
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def square_metres(value: Area) -> float:
|
|
158
|
+
if not isinstance(value, Area):
|
|
159
|
+
raise TypeError("square_metres expects Area")
|
|
160
|
+
return value.mm2 / 1_000_000.0
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def cubic_metres(value: Volume) -> float:
|
|
164
|
+
if not isinstance(value, Volume):
|
|
165
|
+
raise TypeError("cubic_metres expects Volume")
|
|
166
|
+
return value.mm3 / 1_000_000_000.0
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def format_feet_inches(length_mm: float, denominator: int = 16) -> str:
|
|
170
|
+
total_inches = length_mm / 25.4
|
|
171
|
+
feet_part = int(total_inches // 12)
|
|
172
|
+
inches_part = total_inches - feet_part * 12
|
|
173
|
+
rounded = round(inches_part * denominator) / denominator
|
|
174
|
+
if rounded >= 12:
|
|
175
|
+
feet_part += 1
|
|
176
|
+
rounded = 0
|
|
177
|
+
whole = int(rounded)
|
|
178
|
+
fraction = rounded - whole
|
|
179
|
+
if fraction:
|
|
180
|
+
numerator = round(fraction * denominator)
|
|
181
|
+
from math import gcd
|
|
182
|
+
|
|
183
|
+
divisor = gcd(numerator, denominator)
|
|
184
|
+
inch_text = f"{whole} {numerator // divisor}/{denominator // divisor}"
|
|
185
|
+
else:
|
|
186
|
+
inch_text = str(whole)
|
|
187
|
+
return f"{feet_part}'-{inch_text}\""
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
__all__ = [
|
|
191
|
+
"Area",
|
|
192
|
+
"FOOT",
|
|
193
|
+
"INCH",
|
|
194
|
+
"Length",
|
|
195
|
+
"MM",
|
|
196
|
+
"Volume",
|
|
197
|
+
"cubic_feet",
|
|
198
|
+
"cubic_yards",
|
|
199
|
+
"feet",
|
|
200
|
+
"feet_inches",
|
|
201
|
+
"inches",
|
|
202
|
+
"metres",
|
|
203
|
+
"mm",
|
|
204
|
+
"square_feet",
|
|
205
|
+
"square_metres",
|
|
206
|
+
"to_mm",
|
|
207
|
+
]
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Public validation issue type and shared-model contract validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from math import isfinite
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
from .elements import DesignElement, DesignModel, MaterialSpec
|
|
11
|
+
from .geometry import is_valid_solid
|
|
12
|
+
|
|
13
|
+
_ARTIFACT_STEM = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,127}\Z", re.ASCII)
|
|
14
|
+
_RESERVED_STEMS = {
|
|
15
|
+
"CON",
|
|
16
|
+
"PRN",
|
|
17
|
+
"AUX",
|
|
18
|
+
"NUL",
|
|
19
|
+
*(f"COM{number}" for number in range(1, 10)),
|
|
20
|
+
*(f"LPT{number}" for number in range(1, 10)),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class ValidationIssue:
|
|
26
|
+
code: str
|
|
27
|
+
severity: Literal["warning", "error"]
|
|
28
|
+
message: str
|
|
29
|
+
source_path: str | None = None
|
|
30
|
+
element_id: str | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def validate_model(model: DesignModel) -> list[ValidationIssue]:
|
|
34
|
+
issues: list[ValidationIssue] = []
|
|
35
|
+
if not model.id:
|
|
36
|
+
issues.append(ValidationIssue("model-id", "error", "Model ID must be nonempty"))
|
|
37
|
+
if not model.name:
|
|
38
|
+
issues.append(ValidationIssue("model-name", "error", "Model display name must be nonempty"))
|
|
39
|
+
if not valid_artifact_stem(model.artifact_stem):
|
|
40
|
+
issues.append(ValidationIssue("artifact-stem", "error", "Model artifact_stem is not a safe portable filename stem"))
|
|
41
|
+
|
|
42
|
+
walked = list(model.walk())
|
|
43
|
+
known = {element.id for element in walked}
|
|
44
|
+
seen_ids: set[str] = set()
|
|
45
|
+
seen_instances: set[int] = set()
|
|
46
|
+
for element in walked:
|
|
47
|
+
issues.extend(_validate_element(element, seen_ids, seen_instances, known))
|
|
48
|
+
for validator in model.validators:
|
|
49
|
+
issues.extend(validator(model))
|
|
50
|
+
return sorted(issues, key=lambda issue: (issue.severity, issue.code, issue.source_path or "", issue.element_id or ""))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def valid_artifact_stem(value: str) -> bool:
|
|
54
|
+
if not isinstance(value, str) or value in {".", ".."} or _ARTIFACT_STEM.fullmatch(value) is None:
|
|
55
|
+
return False
|
|
56
|
+
device = value.split(".", 1)[0].upper()
|
|
57
|
+
return device not in _RESERVED_STEMS
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _validate_element(
|
|
61
|
+
element: DesignElement,
|
|
62
|
+
seen_ids: set[str],
|
|
63
|
+
seen_instances: set[int],
|
|
64
|
+
known: set[str],
|
|
65
|
+
) -> list[ValidationIssue]:
|
|
66
|
+
issues: list[ValidationIssue] = []
|
|
67
|
+
instance_id = id(element)
|
|
68
|
+
if instance_id in seen_instances:
|
|
69
|
+
issues.append(ValidationIssue("shared-element", "error", "Element instances must not be shared", element_id=element.id))
|
|
70
|
+
seen_instances.add(instance_id)
|
|
71
|
+
if not element.id or element.id in seen_ids:
|
|
72
|
+
issues.append(ValidationIssue("stable-id", "error", "Stable IDs must be nonempty and unique", element_id=element.id or None))
|
|
73
|
+
seen_ids.add(element.id)
|
|
74
|
+
if not element.name:
|
|
75
|
+
issues.append(ValidationIssue("element-name", "error", "Element display names must be nonempty", element_id=element.id))
|
|
76
|
+
values = (*element.placement.translation_mm, *element.placement.rotation_deg_xyz)
|
|
77
|
+
if not all(isfinite(value) for value in values):
|
|
78
|
+
issues.append(ValidationIssue("placement", "error", "Placement values must be finite", element_id=element.id))
|
|
79
|
+
if element.geometry is None:
|
|
80
|
+
issues.append(ValidationIssue("geometry-missing", "error", "Geometry must be present", element_id=element.id))
|
|
81
|
+
if element.physical and element.geometry_kind != "solid":
|
|
82
|
+
issues.append(ValidationIssue("physical-kind", "error", "Physical elements must be solids", element_id=element.id))
|
|
83
|
+
if element.physical and element.geometry is not None and not is_valid_solid(element.geometry):
|
|
84
|
+
issues.append(
|
|
85
|
+
ValidationIssue("solid-invalid", "error", "Physical geometry must be a valid positive-volume solid", element_id=element.id)
|
|
86
|
+
)
|
|
87
|
+
if not element.physical and element.geometry_kind == "solid":
|
|
88
|
+
issues.append(
|
|
89
|
+
ValidationIssue(
|
|
90
|
+
"reference-solid",
|
|
91
|
+
"warning",
|
|
92
|
+
"Non-physical reference geometry is represented as a solid",
|
|
93
|
+
element_id=element.id,
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
if any(value <= 0 or not isfinite(value) for value in element.dimensions.values()):
|
|
97
|
+
issues.append(
|
|
98
|
+
ValidationIssue("dimensions", "error", "Declared dimensions must be finite and positive", element_id=element.id)
|
|
99
|
+
)
|
|
100
|
+
if element.physical and element.material is None:
|
|
101
|
+
issues.append(ValidationIssue("material", "error", "Physical elements require a material", element_id=element.id))
|
|
102
|
+
if element.material is not None:
|
|
103
|
+
issues.extend(_validate_material(element.material, element.id))
|
|
104
|
+
color = element.color_rgb
|
|
105
|
+
if color is not None and not _valid_color(color):
|
|
106
|
+
issues.append(ValidationIssue("color", "error", "Colors must contain three finite values in [0, 1]", element_id=element.id))
|
|
107
|
+
mapping = element.ifc_mapping
|
|
108
|
+
mapped = isinstance(mapping.ifc_class, str) and bool(mapping.ifc_class.strip())
|
|
109
|
+
excluded = isinstance(mapping.exclude_reason, str) and bool(mapping.exclude_reason.strip())
|
|
110
|
+
if mapped == excluded:
|
|
111
|
+
issues.append(
|
|
112
|
+
ValidationIssue(
|
|
113
|
+
"ifc-intent",
|
|
114
|
+
"error",
|
|
115
|
+
"IFC intent must contain exactly one mapping class or exclusion reason",
|
|
116
|
+
element_id=element.id,
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
if element.parent_id is not None and element.parent_id not in known:
|
|
120
|
+
issues.append(ValidationIssue("parent", "error", f"Unknown parent ID: {element.parent_id}", element_id=element.id))
|
|
121
|
+
return issues
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _validate_material(material: MaterialSpec, element_id: str) -> list[ValidationIssue]:
|
|
125
|
+
issues: list[ValidationIssue] = []
|
|
126
|
+
if not material.id or not material.name or not material.category:
|
|
127
|
+
issues.append(
|
|
128
|
+
ValidationIssue("material-fields", "error", "Material ID, name, and category must be nonempty", element_id=element_id)
|
|
129
|
+
)
|
|
130
|
+
if not _valid_color(material.color_rgb):
|
|
131
|
+
issues.append(
|
|
132
|
+
ValidationIssue("material-color", "error", "Material color must contain three finite values in [0, 1]", element_id=element_id)
|
|
133
|
+
)
|
|
134
|
+
if material.density_kg_m3 is not None and (not isfinite(material.density_kg_m3) or material.density_kg_m3 <= 0):
|
|
135
|
+
issues.append(
|
|
136
|
+
ValidationIssue("material-density", "error", "Material density must be finite and positive", element_id=element_id)
|
|
137
|
+
)
|
|
138
|
+
return issues
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _valid_color(color: object) -> bool:
|
|
142
|
+
return (
|
|
143
|
+
isinstance(color, tuple)
|
|
144
|
+
and len(color) == 3
|
|
145
|
+
and all(isinstance(value, (int, float)) and isfinite(value) and 0 <= value <= 1 for value in color)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def fatal_issues(issues: list[ValidationIssue] | tuple[ValidationIssue, ...]) -> list[ValidationIssue]:
|
|
150
|
+
return [issue for issue in issues if issue.severity == "error"]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
__all__ = ["ValidationIssue"]
|