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.
Files changed (236) hide show
  1. python_cad_tools/__init__.py +10 -0
  2. python_cad_tools/_locking.py +63 -0
  3. python_cad_tools/_project_loader.py +273 -0
  4. python_cad_tools/_project_state.py +65 -0
  5. python_cad_tools/_scaffold.py +66 -0
  6. python_cad_tools/_transaction.py +208 -0
  7. python_cad_tools/_viewer_static/app.webmanifest +18 -0
  8. python_cad_tools/_viewer_static/asset-manifest.json +1 -0
  9. python_cad_tools/_viewer_static/assets/animationGroup-Sd34ntWg.js +1 -0
  10. python_cad_tools/_viewer_static/assets/bakedVertexAnimation-Cd_pRdrn.js +316 -0
  11. python_cad_tools/_viewer_static/assets/basisTextureLoader-DWgvS_1G.js +1 -0
  12. python_cad_tools/_viewer_static/assets/brdfTextureTools-CWGiG1W2.js +2 -0
  13. python_cad_tools/_viewer_static/assets/bumpFragment-CWULheF1.js +127 -0
  14. python_cad_tools/_viewer_static/assets/bumpVertex-DqzFZayE.js +6 -0
  15. python_cad_tools/_viewer_static/assets/bumpVertexDeclaration-C5EFyTio.js +6 -0
  16. python_cad_tools/_viewer_static/assets/clipPlaneFragment-BXRtt4jj.js +46 -0
  17. python_cad_tools/_viewer_static/assets/clipPlaneVertex-B_VhD1oQ.js +37 -0
  18. python_cad_tools/_viewer_static/assets/cubemapToSphericalPolynomial-DRpaukza.js +1 -0
  19. python_cad_tools/_viewer_static/assets/dds-Dkb2Cx2N.js +1 -0
  20. python_cad_tools/_viewer_static/assets/dds.pure-DtvJsKvY.js +1 -0
  21. python_cad_tools/_viewer_static/assets/ddsTextureLoader-H78xux1g.js +1 -0
  22. python_cad_tools/_viewer_static/assets/default.fragment-oSQ-tWhe.js +469 -0
  23. python_cad_tools/_viewer_static/assets/default.vertex-DhFjw_Da.js +209 -0
  24. python_cad_tools/_viewer_static/assets/defaultUboDeclaration-BMDHNppx.js +5 -0
  25. python_cad_tools/_viewer_static/assets/dumpTools-BaNZPjUX.js +2 -0
  26. python_cad_tools/_viewer_static/assets/envTextureLoader-V0YnLcIM.js +1 -0
  27. python_cad_tools/_viewer_static/assets/exrTextureLoader-CjGyIK3x.js +1 -0
  28. python_cad_tools/_viewer_static/assets/flowGraphApplyForceBlock-CW5nuobR.js +1 -0
  29. python_cad_tools/_viewer_static/assets/flowGraphApplyImpulseBlock-LFqYIkhj.js +1 -0
  30. python_cad_tools/_viewer_static/assets/flowGraphArrayIndexBlock-BMOHUbBm.js +1 -0
  31. python_cad_tools/_viewer_static/assets/flowGraphBezierCurveEasingBlock-knmTiuEk.js +1 -0
  32. python_cad_tools/_viewer_static/assets/flowGraphBinaryOperationBlock-DFLuCAKJ.js +1 -0
  33. python_cad_tools/_viewer_static/assets/flowGraphBranchBlock-BJRHMXMo.js +1 -0
  34. python_cad_tools/_viewer_static/assets/flowGraphCachedOperationBlock-Dng-6jE0.js +1 -0
  35. python_cad_tools/_viewer_static/assets/flowGraphCancelDelayBlock-CgbK3CQZ.js +1 -0
  36. python_cad_tools/_viewer_static/assets/flowGraphCodeExecutionBlock-8zMtq9tg.js +1 -0
  37. python_cad_tools/_viewer_static/assets/flowGraphConditionalDataBlock-BiLr2rkI.js +1 -0
  38. python_cad_tools/_viewer_static/assets/flowGraphConsoleLogBlock-J1vcCtNP.js +1 -0
  39. python_cad_tools/_viewer_static/assets/flowGraphConstantBlock-CFb5nuaV.js +1 -0
  40. python_cad_tools/_viewer_static/assets/flowGraphContextBlock-DAeGVzhC.js +1 -0
  41. python_cad_tools/_viewer_static/assets/flowGraphCounterBlock-lZlAQiJd.js +1 -0
  42. python_cad_tools/_viewer_static/assets/flowGraphDataSwitchBlock-BIzvmbVy.js +1 -0
  43. python_cad_tools/_viewer_static/assets/flowGraphDebounceBlock-DvNuMVwy.js +1 -0
  44. python_cad_tools/_viewer_static/assets/flowGraphDebugBlock-Dymd2VEE.js +1 -0
  45. python_cad_tools/_viewer_static/assets/flowGraphDoNBlock-v68aoNLH.js +1 -0
  46. python_cad_tools/_viewer_static/assets/flowGraphEasingBlock-CTnGv92q.js +1 -0
  47. python_cad_tools/_viewer_static/assets/flowGraphFlipFlopBlock-DXsO0Chw.js +1 -0
  48. python_cad_tools/_viewer_static/assets/flowGraphForLoopBlock-CPpe9T75.js +1 -0
  49. python_cad_tools/_viewer_static/assets/flowGraphFunctionReferenceBlock-DjDev4Pq.js +1 -0
  50. python_cad_tools/_viewer_static/assets/flowGraphGLTFDataProvider-GIYCz4AQ.js +1 -0
  51. python_cad_tools/_viewer_static/assets/flowGraphGetAngularVelocityBlock-BStkOVeL.js +1 -0
  52. python_cad_tools/_viewer_static/assets/flowGraphGetAssetBlock-Eiqoivbz.js +1 -0
  53. python_cad_tools/_viewer_static/assets/flowGraphGetLinearVelocityBlock-B0cpV5ws.js +1 -0
  54. python_cad_tools/_viewer_static/assets/flowGraphGetPhysicsMassPropertiesBlock-B-H64yaI.js +1 -0
  55. python_cad_tools/_viewer_static/assets/flowGraphGetPropertyBlock-CUyOsPw0.js +1 -0
  56. python_cad_tools/_viewer_static/assets/flowGraphGetSoundVolumeBlock-C0FjrsUz.js +1 -0
  57. python_cad_tools/_viewer_static/assets/flowGraphGetVariableBlock-O4597zBl.js +1 -0
  58. python_cad_tools/_viewer_static/assets/flowGraphIndexOfBlock-E00Z3KXM.js +1 -0
  59. python_cad_tools/_viewer_static/assets/flowGraphInterpolationBlock-DnkA9MNH.js +1 -0
  60. python_cad_tools/_viewer_static/assets/flowGraphIsKeyPressedBlock-DDfeuAyf.js +1 -0
  61. python_cad_tools/_viewer_static/assets/flowGraphIsSoundPlayingBlock-DMXg8jdM.js +1 -0
  62. python_cad_tools/_viewer_static/assets/flowGraphJsonPointerParserBlock-OmCorK0y.js +1 -0
  63. python_cad_tools/_viewer_static/assets/flowGraphKeyDownEventBlock-RuVtj6g2.js +1 -0
  64. python_cad_tools/_viewer_static/assets/flowGraphKeyUpEventBlock-DO4pAfHi.js +1 -0
  65. python_cad_tools/_viewer_static/assets/flowGraphKeyboardEventBlock-Bx3pAzzh.js +1 -0
  66. python_cad_tools/_viewer_static/assets/flowGraphMathBlocks-7W1XYOC8.js +1 -0
  67. python_cad_tools/_viewer_static/assets/flowGraphMathCombineExtractBlocks-a4f0Kv6e.js +1 -0
  68. python_cad_tools/_viewer_static/assets/flowGraphMatrixMathBlocks-C5Z-y1Au.js +1 -0
  69. python_cad_tools/_viewer_static/assets/flowGraphMeshPickEventBlock-D4m-I4CS.js +1 -0
  70. python_cad_tools/_viewer_static/assets/flowGraphMultiGateBlock-BISVSMw6.js +1 -0
  71. python_cad_tools/_viewer_static/assets/flowGraphPauseAnimationBlock-BYP2Z-HG.js +1 -0
  72. python_cad_tools/_viewer_static/assets/flowGraphPauseSoundBlock-Drh1EeW6.js +1 -0
  73. python_cad_tools/_viewer_static/assets/flowGraphPhysicsCollisionEventBlock-BoziAQ75.js +1 -0
  74. python_cad_tools/_viewer_static/assets/flowGraphPlayAnimationBlock-Clry-68M.js +1 -0
  75. python_cad_tools/_viewer_static/assets/flowGraphPlaySoundBlock-LbBVxGBE.js +1 -0
  76. python_cad_tools/_viewer_static/assets/flowGraphPointerDownEventBlock-DIVXCSHh.js +1 -0
  77. python_cad_tools/_viewer_static/assets/flowGraphPointerMoveEventBlock-BNNOzY2W.js +1 -0
  78. python_cad_tools/_viewer_static/assets/flowGraphPointerOutEventBlock-B8f2dOFT.js +1 -0
  79. python_cad_tools/_viewer_static/assets/flowGraphPointerOverEventBlock-CQr8LCee.js +1 -0
  80. python_cad_tools/_viewer_static/assets/flowGraphPointerUpEventBlock-DMg_iEHl.js +1 -0
  81. python_cad_tools/_viewer_static/assets/flowGraphReceiveCustomEventBlock-B8GJm9Ai.js +1 -0
  82. python_cad_tools/_viewer_static/assets/flowGraphSceneReadyEventBlock-Dcdb29K2.js +1 -0
  83. python_cad_tools/_viewer_static/assets/flowGraphSceneTickEventBlock-BLE5p4mi.js +1 -0
  84. python_cad_tools/_viewer_static/assets/flowGraphSendCustomEventBlock-BMzDItQ5.js +1 -0
  85. python_cad_tools/_viewer_static/assets/flowGraphSequenceBlock-DRURovjn.js +1 -0
  86. python_cad_tools/_viewer_static/assets/flowGraphSetAngularVelocityBlock-Ba8dhkLM.js +1 -0
  87. python_cad_tools/_viewer_static/assets/flowGraphSetDelayBlock-CnQR--Gn.js +1 -0
  88. python_cad_tools/_viewer_static/assets/flowGraphSetLinearVelocityBlock-Dm1r3vkh.js +1 -0
  89. python_cad_tools/_viewer_static/assets/flowGraphSetPhysicsMotionTypeBlock-BNtMLimi.js +1 -0
  90. python_cad_tools/_viewer_static/assets/flowGraphSetPropertyBlock-C-uv2jhP.js +1 -0
  91. python_cad_tools/_viewer_static/assets/flowGraphSetSoundVolumeBlock-D9LDx3lg.js +1 -0
  92. python_cad_tools/_viewer_static/assets/flowGraphSetVariableBlock-DszzRB5X.js +1 -0
  93. python_cad_tools/_viewer_static/assets/flowGraphSoundEndedEventBlock-DeTDSudb.js +1 -0
  94. python_cad_tools/_viewer_static/assets/flowGraphStopAnimationBlock-DWuwoFWr.js +1 -0
  95. python_cad_tools/_viewer_static/assets/flowGraphStopSoundBlock-DEnj2cQw.js +1 -0
  96. python_cad_tools/_viewer_static/assets/flowGraphSwitchBlock-zs8Fp9ky.js +1 -0
  97. python_cad_tools/_viewer_static/assets/flowGraphThrottleBlock-BYAKUQXd.js +1 -0
  98. python_cad_tools/_viewer_static/assets/flowGraphTransformCoordinatesSystemBlock-DYP54m5P.js +1 -0
  99. python_cad_tools/_viewer_static/assets/flowGraphTypeToTypeBlocks-vFwn79bS.js +1 -0
  100. python_cad_tools/_viewer_static/assets/flowGraphUnaryOperationBlock-CaRh4Ef0.js +1 -0
  101. python_cad_tools/_viewer_static/assets/flowGraphVectorMathBlocks-BEIhMs-A.js +1 -0
  102. python_cad_tools/_viewer_static/assets/flowGraphWaitAllBlock-DFbELP_j.js +1 -0
  103. python_cad_tools/_viewer_static/assets/flowGraphWhileLoopBlock-C1OKQ-_F.js +1 -0
  104. python_cad_tools/_viewer_static/assets/fogFragment-DtDlQBnz.js +25 -0
  105. python_cad_tools/_viewer_static/assets/gaussianSplatting-Cfigjb15.js +242 -0
  106. python_cad_tools/_viewer_static/assets/gaussianSplatting-DIn8R8oF.js +296 -0
  107. python_cad_tools/_viewer_static/assets/gaussianSplatting.fragment-BxeRJ2k-.js +38 -0
  108. python_cad_tools/_viewer_static/assets/gaussianSplatting.fragment-DNDmvRs-.js +37 -0
  109. python_cad_tools/_viewer_static/assets/gaussianSplatting.vertex-D92z5jYn.js +86 -0
  110. python_cad_tools/_viewer_static/assets/gaussianSplatting.vertex-DSWfFnKQ.js +86 -0
  111. python_cad_tools/_viewer_static/assets/gaussianSplattingVoxel.fragment-DBQE2gDk.js +12 -0
  112. python_cad_tools/_viewer_static/assets/gaussianSplattingVoxel.fragment-qwx5ntqJ.js +17 -0
  113. python_cad_tools/_viewer_static/assets/gaussianSplattingVoxel.vertex-CP2kd3jU.js +30 -0
  114. python_cad_tools/_viewer_static/assets/gaussianSplattingVoxel.vertex-Dr-fEg_W.js +19 -0
  115. python_cad_tools/_viewer_static/assets/geometry.fragment-CsJIm2H0.js +242 -0
  116. python_cad_tools/_viewer_static/assets/geometry.fragment-Dph3muZt.js +262 -0
  117. python_cad_tools/_viewer_static/assets/geometry.vertex-BXGMTA_J.js +212 -0
  118. python_cad_tools/_viewer_static/assets/geometry.vertex-Bm9Wg3Gi.js +214 -0
  119. python_cad_tools/_viewer_static/assets/glowBlurPostProcess.fragment-BJSwKIJD.js +7 -0
  120. python_cad_tools/_viewer_static/assets/glowMapGeneration.fragment-DcyS1cTF.js +63 -0
  121. python_cad_tools/_viewer_static/assets/glowMapGeneration.vertex-BGq1Fakj.js +75 -0
  122. python_cad_tools/_viewer_static/assets/glowMapMerge.fragment-DxcneU_F.js +25 -0
  123. python_cad_tools/_viewer_static/assets/glowMapMerge.vertex-DaJJKqMV.js +8 -0
  124. python_cad_tools/_viewer_static/assets/harmonicsFunctions-DzJcvNWG.js +16 -0
  125. python_cad_tools/_viewer_static/assets/hdrTextureLoader-BGQvgh0h.js +2 -0
  126. python_cad_tools/_viewer_static/assets/helperFunctions-Cugfp1Fz.js +87 -0
  127. python_cad_tools/_viewer_static/assets/iesTextureLoader-CO4G0p2M.js +2 -0
  128. python_cad_tools/_viewer_static/assets/index-B2zBEWsq.js +7007 -0
  129. python_cad_tools/_viewer_static/assets/index-DcTTrc-8.css +1 -0
  130. python_cad_tools/_viewer_static/assets/kernelBlur.fragment-B6CTXTxY.js +57 -0
  131. python_cad_tools/_viewer_static/assets/kernelBlur.fragment-BkKbudbW.js +57 -0
  132. python_cad_tools/_viewer_static/assets/kernelBlur.vertex-BYwFU5IS.js +11 -0
  133. python_cad_tools/_viewer_static/assets/kernelBlur.vertex-D371zUfT.js +11 -0
  134. python_cad_tools/_viewer_static/assets/kernelBlurVaryingDeclaration-CgBGaCHg.js +1 -0
  135. python_cad_tools/_viewer_static/assets/kernelBlurVaryingDeclaration-YS8JpqdO.js +1 -0
  136. python_cad_tools/_viewer_static/assets/ktxTextureLoader-ncMSJTjl.js +1 -0
  137. python_cad_tools/_viewer_static/assets/lightFragment-skHiFbKB.js +398 -0
  138. python_cad_tools/_viewer_static/assets/logDepthDeclaration-BR8XRz9F.js +4 -0
  139. python_cad_tools/_viewer_static/assets/logDepthVertex-UKrhEIvg.js +14 -0
  140. python_cad_tools/_viewer_static/assets/mainUVVaryingDeclaration-BxQWGFcG.js +4 -0
  141. python_cad_tools/_viewer_static/assets/mesh.vertexData.functions-ayNlGC5K.js +1 -0
  142. python_cad_tools/_viewer_static/assets/meshUboDeclaration-BnX046aQ.js +4 -0
  143. python_cad_tools/_viewer_static/assets/oitFragment-CTMCOfNP.js +605 -0
  144. python_cad_tools/_viewer_static/assets/openpbr.fragment-DPJPqi6p.js +1530 -0
  145. python_cad_tools/_viewer_static/assets/openpbr.fragment-Dv6qTVVi.js +1384 -0
  146. python_cad_tools/_viewer_static/assets/openpbr.vertex-Bn2wrdGL.js +364 -0
  147. python_cad_tools/_viewer_static/assets/openpbr.vertex-CHESW3gB.js +251 -0
  148. python_cad_tools/_viewer_static/assets/openpbrMaterial.pure-BdK7VgO_.js +3 -0
  149. python_cad_tools/_viewer_static/assets/openpbrMaterialLoadingAdapter-Bm_AHDUN.js +4 -0
  150. python_cad_tools/_viewer_static/assets/openpbrTransmissionLayerData-BDa9qEoN.js +339 -0
  151. python_cad_tools/_viewer_static/assets/openpbrTransmissionLayerData-Ba_ly4LE.js +343 -0
  152. python_cad_tools/_viewer_static/assets/openpbrUboDeclaration-BY2lSH2C.js +5 -0
  153. python_cad_tools/_viewer_static/assets/openpbrUboDeclaration-DajhikDS.js +6 -0
  154. python_cad_tools/_viewer_static/assets/packingFunctions-BHPwcGG2.js +4 -0
  155. python_cad_tools/_viewer_static/assets/packingFunctions-DTwzBRBx.js +4 -0
  156. python_cad_tools/_viewer_static/assets/pass.fragment-9oKU4pI3.js +4 -0
  157. python_cad_tools/_viewer_static/assets/passCube.fragment-CpAeiYx1.js +23 -0
  158. python_cad_tools/_viewer_static/assets/passCube.fragment-DHtSWVRF.js +23 -0
  159. python_cad_tools/_viewer_static/assets/pbr.fragment-cIhMKwLs.js +2560 -0
  160. python_cad_tools/_viewer_static/assets/pbr.vertex-BfdONsf2.js +255 -0
  161. python_cad_tools/_viewer_static/assets/pbrBRDFFunctions-B_ppOzPO.js +173 -0
  162. python_cad_tools/_viewer_static/assets/pbrDebug-DDdwL5_t.js +693 -0
  163. python_cad_tools/_viewer_static/assets/pbrIBLFunctions-b1zdpzOP.js +43 -0
  164. python_cad_tools/_viewer_static/assets/pbrMaterial.pure-BZA7yVhu.js +2 -0
  165. python_cad_tools/_viewer_static/assets/pbrMaterialLoadingAdapter-BRvKRnjl.js +1 -0
  166. python_cad_tools/_viewer_static/assets/pbrUboDeclaration-CMZp25k3.js +5 -0
  167. python_cad_tools/_viewer_static/assets/postprocess.vertex-DzdUpNbW.js +9 -0
  168. python_cad_tools/_viewer_static/assets/procedural.vertex-BnbuQ-1f.js +7 -0
  169. python_cad_tools/_viewer_static/assets/procedural.vertex-Dp-Ng7Pc.js +8 -0
  170. python_cad_tools/_viewer_static/assets/reflectionFunction-DBlr3oDs.js +83 -0
  171. python_cad_tools/_viewer_static/assets/rgbdDecode.fragment-CyvnZFOJ.js +5 -0
  172. python_cad_tools/_viewer_static/assets/rgbdEncode.fragment-Dz4zLnoD.js +5 -0
  173. python_cad_tools/_viewer_static/assets/rgbdEncode.fragment-iscgpA1_.js +5 -0
  174. python_cad_tools/_viewer_static/assets/sceneUboDeclaration-BzFIc4mb.js +11 -0
  175. python_cad_tools/_viewer_static/assets/textureProcessor.fragment-C9D0rd-M.js +151 -0
  176. python_cad_tools/_viewer_static/assets/textureProcessor.fragment-JMKyWZng.js +146 -0
  177. python_cad_tools/_viewer_static/assets/tgaTextureLoader-oxTElIQJ.js +1 -0
  178. python_cad_tools/_viewer_static/assets/vertexColorMixing-mdC9xZe1.js +418 -0
  179. python_cad_tools/_viewer_static/favicon.svg +10 -0
  180. python_cad_tools/_viewer_static/index.html +18 -0
  181. python_cad_tools/_viewer_static/sw.js +61 -0
  182. python_cad_tools/artifacts.py +546 -0
  183. python_cad_tools/build-info.json +1 -0
  184. python_cad_tools/build.py +270 -0
  185. python_cad_tools/cli.py +212 -0
  186. python_cad_tools/context.py +67 -0
  187. python_cad_tools/determinism.py +66 -0
  188. python_cad_tools/drawings/__init__.py +124 -0
  189. python_cad_tools/drawings/_catalog.py +79 -0
  190. python_cad_tools/drawings/_validation.py +238 -0
  191. python_cad_tools/elements.py +121 -0
  192. python_cad_tools/exceptions.py +45 -0
  193. python_cad_tools/exporters/__init__.py +8 -0
  194. python_cad_tools/exporters/base.py +24 -0
  195. python_cad_tools/exporters/drawings/__init__.py +5 -0
  196. python_cad_tools/exporters/drawings/annotations.py +444 -0
  197. python_cad_tools/exporters/drawings/dxf.py +110 -0
  198. python_cad_tools/exporters/drawings/model.py +70 -0
  199. python_cad_tools/exporters/drawings/pdf.py +119 -0
  200. python_cad_tools/exporters/drawings/primitives.py +93 -0
  201. python_cad_tools/exporters/drawings/projection.py +64 -0
  202. python_cad_tools/exporters/drawings/svg.py +135 -0
  203. python_cad_tools/exporters/glb.py +100 -0
  204. python_cad_tools/exporters/ifc.py +229 -0
  205. python_cad_tools/exporters/quantities.py +136 -0
  206. python_cad_tools/exporters/step.py +117 -0
  207. python_cad_tools/geometry.py +187 -0
  208. python_cad_tools/manifests.py +338 -0
  209. python_cad_tools/project_config.py +174 -0
  210. python_cad_tools/py.typed +1 -0
  211. python_cad_tools/runtime_sync.py +126 -0
  212. python_cad_tools/schemas/build-manifest-1.json +1 -0
  213. python_cad_tools/schemas/design-manifest-1.json +1 -0
  214. python_cad_tools/schemas/download-manifest-2.json +46 -0
  215. python_cad_tools/schemas/drawing-annotations-1.json +66 -0
  216. python_cad_tools/schemas/glb-manifest-1.json +1 -0
  217. python_cad_tools/schemas/ifc-validation-1.json +1 -0
  218. python_cad_tools/schemas/quantities-1.json +1 -0
  219. python_cad_tools/schemas/run-metadata-1.json +1 -0
  220. python_cad_tools/schemas/site-metadata-1.json +34 -0
  221. python_cad_tools/schemas/step-validation-1.json +1 -0
  222. python_cad_tools/site.py +676 -0
  223. python_cad_tools/templates/project/.gitignore +9 -0
  224. python_cad_tools/templates/project/README.md +38 -0
  225. python_cad_tools/templates/project/config.py +36 -0
  226. python_cad_tools/templates/project/drawing_annotations.py +117 -0
  227. python_cad_tools/templates/project/model.py +87 -0
  228. python_cad_tools/templates/project/pyproject.toml +27 -0
  229. python_cad_tools/units.py +207 -0
  230. python_cad_tools/validation.py +153 -0
  231. python_cad_tools-0.1.0rc1.dist-info/METADATA +149 -0
  232. python_cad_tools-0.1.0rc1.dist-info/RECORD +236 -0
  233. python_cad_tools-0.1.0rc1.dist-info/WHEEL +5 -0
  234. python_cad_tools-0.1.0rc1.dist-info/entry_points.txt +2 -0
  235. python_cad_tools-0.1.0rc1.dist-info/licenses/LICENSE +679 -0
  236. python_cad_tools-0.1.0rc1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,229 @@
1
+ """IFC4 authoring from Python CAD Tools elements using deterministic IDs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import uuid
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import ifcopenshell
11
+ import ifcopenshell.guid
12
+ import ifcopenshell.validate
13
+
14
+ from ..determinism import normalized_build_epoch, write_json
15
+ from ..exceptions import BuildExportError
16
+ from ..geometry import tessellate
17
+ from .base import selected_elements
18
+
19
+ IFC_NAMESPACE = uuid.UUID("8ef8d46f-f692-5a1f-9966-c498829777a9")
20
+
21
+
22
+ def global_id(stable_id: str) -> str:
23
+ return ifcopenshell.guid.compress(uuid.uuid5(IFC_NAMESPACE, stable_id).hex)
24
+
25
+
26
+ class IfcExporter:
27
+ name = "ifc"
28
+
29
+ def export(self, model, output_dir: Path) -> list[Path]:
30
+ target = output_dir / "ifc"
31
+ target.mkdir(parents=True, exist_ok=True)
32
+ path = target / f"{model.artifact_stem}.ifc"
33
+ file = ifcopenshell.file(schema="IFC4")
34
+ origin = file.createIfcCartesianPoint((0.0, 0.0, 0.0))
35
+ z_axis = file.createIfcDirection((0.0, 0.0, 1.0))
36
+ x_axis = file.createIfcDirection((1.0, 0.0, 0.0))
37
+ world = file.createIfcAxis2Placement3D(origin, z_axis, x_axis)
38
+ context = file.create_entity(
39
+ "IfcGeometricRepresentationContext",
40
+ ContextIdentifier="Model",
41
+ ContextType="Model",
42
+ CoordinateSpaceDimension=3,
43
+ Precision=1e-5,
44
+ WorldCoordinateSystem=world,
45
+ )
46
+ units = file.createIfcUnitAssignment(
47
+ (
48
+ file.createIfcSIUnit(None, "LENGTHUNIT", "MILLI", "METRE"),
49
+ file.createIfcSIUnit(None, "AREAUNIT", None, "SQUARE_METRE"),
50
+ file.createIfcSIUnit(None, "VOLUMEUNIT", None, "CUBIC_METRE"),
51
+ )
52
+ )
53
+ project = file.create_entity(
54
+ "IfcProject",
55
+ GlobalId=global_id(f"{model.id}.ifc.project"),
56
+ Name=model.name,
57
+ RepresentationContexts=[context],
58
+ UnitsInContext=units,
59
+ )
60
+ site = _spatial(file, "IfcSite", f"{model.id}.ifc.site", "Default Site", None)
61
+ building = _spatial(file, "IfcBuilding", f"{model.id}.ifc.building", "Default Building", site.ObjectPlacement)
62
+ storey = _spatial(file, "IfcBuildingStorey", f"{model.id}.ifc.storey", "Default Storey", building.ObjectPlacement)
63
+ file.createIfcRelAggregates(global_id(f"{model.id}.rel.project-site"), None, None, None, project, [site])
64
+ file.createIfcRelAggregates(global_id(f"{model.id}.rel.site-building"), None, None, None, site, [building])
65
+ file.createIfcRelAggregates(global_id(f"{model.id}.rel.building-storey"), None, None, None, building, [storey])
66
+
67
+ products = []
68
+ expected_ids: list[str] = []
69
+ for element in selected_elements(model, "ifc", physical_only=True):
70
+ if element.ifc_mapping.ifc_class is None:
71
+ continue
72
+ if element.ifc_mapping.ifc_class != "IfcBuildingElementProxy":
73
+ raise BuildExportError(f"Unsupported starter IFC mapping: {element.ifc_mapping.ifc_class}")
74
+ vertices, triangles = tessellate(element.geometry, 0.5, 0.25)
75
+ points = file.createIfcCartesianPointList3D(vertices)
76
+ face_set = file.createIfcTriangulatedFaceSet(
77
+ points,
78
+ None,
79
+ True,
80
+ [[index + 1 for index in triangle] for triangle in triangles],
81
+ None,
82
+ )
83
+ representation = file.createIfcShapeRepresentation(context, "Body", "Tessellation", [face_set])
84
+ shape = file.createIfcProductDefinitionShape(None, None, [representation])
85
+ product = file.create_entity(
86
+ "IfcBuildingElementProxy",
87
+ GlobalId=global_id(element.id),
88
+ Name=element.name,
89
+ ObjectType=element.category,
90
+ ObjectPlacement=_local_placement(file, storey.ObjectPlacement),
91
+ Representation=shape,
92
+ PredefinedType=element.ifc_mapping.predefined_type or "NOTDEFINED",
93
+ )
94
+ products.append(product)
95
+ expected_ids.append(element.id)
96
+ _add_properties(file, product, element)
97
+ if element.material:
98
+ material = file.createIfcMaterial(element.material.name, element.material.category, None)
99
+ file.createIfcRelAssociatesMaterial(
100
+ global_id(f"{element.id}.material"), None, None, None, [product], material
101
+ )
102
+ _style_item(file, face_set, element.color_rgb or element.material.color_rgb)
103
+ file.createIfcRelContainedInSpatialStructure(
104
+ global_id(f"{model.id}.rel.storey-elements"), None, None, None, products, storey
105
+ )
106
+ file.write(path)
107
+ _normalize_header(path)
108
+ reloaded = ifcopenshell.open(path)
109
+ actual_ids = sorted(
110
+ property_value(entity, "StableId")
111
+ for entity in reloaded.by_type("IfcBuildingElementProxy")
112
+ )
113
+ if actual_ids != sorted(expected_ids):
114
+ raise BuildExportError(f"IFC stable IDs did not round-trip: {actual_ids}")
115
+ logger = ifcopenshell.validate.json_logger()
116
+ # Population/schema validation is a runtime capability. EXPRESS rule
117
+ # execution is intentionally omitted because that optional upstream
118
+ # path imports pytest, which must remain a development-only dependency.
119
+ ifcopenshell.validate.validate(reloaded, logger, express_rules=False)
120
+ statements = list(logger.statements)
121
+ fatal = [item for item in statements if item.get("level") in {"error", "critical"}]
122
+ coordinate_values = [
123
+ coordinate
124
+ for point_list in reloaded.by_type("IfcCartesianPointList3D")
125
+ for coordinate in point_list.CoordList
126
+ ]
127
+ if coordinate_values:
128
+ ifc_bounds = [
129
+ min(point[index] for point in coordinate_values) for index in range(3)
130
+ ] + [max(point[index] for point in coordinate_values) for index in range(3)]
131
+ else:
132
+ ifc_bounds = [0.0] * 6
133
+ report = write_json(
134
+ target / "validation.json",
135
+ {
136
+ "schema_id": "urn:python-cad-tools:schema:ifc-validation:1",
137
+ "schema_version": 1,
138
+ "schema": reloaded.schema,
139
+ "valid": not fatal,
140
+ "fatal_count": len(fatal),
141
+ "issue_count": len(statements),
142
+ "issues": [_json_safe(item) for item in statements],
143
+ "bounds_mm": [round(value, 6) for value in ifc_bounds],
144
+ "element_global_ids": {
145
+ entity.Name: entity.GlobalId
146
+ for entity in sorted(reloaded.by_type("IfcBuildingElementProxy"), key=lambda item: item.Name)
147
+ },
148
+ },
149
+ )
150
+ if fatal:
151
+ raise BuildExportError(f"IfcOpenShell validation reported {len(fatal)} fatal issue(s); see {report}")
152
+ return [path, report]
153
+
154
+
155
+ def _spatial(file, class_name: str, stable_id: str, name: str, parent_placement):
156
+ return file.create_entity(
157
+ class_name,
158
+ GlobalId=global_id(stable_id),
159
+ Name=name,
160
+ ObjectPlacement=_local_placement(file, parent_placement),
161
+ CompositionType="ELEMENT",
162
+ )
163
+
164
+
165
+ def _local_placement(file, parent):
166
+ point = file.createIfcCartesianPoint((0.0, 0.0, 0.0))
167
+ axis = file.createIfcAxis2Placement3D(point, None, None)
168
+ return file.createIfcLocalPlacement(parent, axis)
169
+
170
+
171
+ def _add_properties(file, product, element) -> None:
172
+ values: dict[str, Any] = {
173
+ "StableId": element.id,
174
+ "Category": element.category,
175
+ "SourceModule": element.source_module,
176
+ "GeometryKind": element.geometry_kind,
177
+ }
178
+ for name in ("length_mm", "width_mm", "height_mm", "radius_mm"):
179
+ value = getattr(element.dimensions, name)
180
+ if value is not None:
181
+ values[name.removesuffix("_mm").title() + "Mm"] = value
182
+ properties = []
183
+ for name, value in sorted(values.items()):
184
+ wrapped = file.createIfcReal(float(value)) if isinstance(value, (float, int)) else file.createIfcLabel(str(value))
185
+ properties.append(file.createIfcPropertySingleValue(name, None, wrapped, None))
186
+ pset = file.createIfcPropertySet(global_id(f"{element.id}.pset"), None, "Pset_PythonCadDesign", None, properties)
187
+ file.createIfcRelDefinesByProperties(global_id(f"{element.id}.pset-rel"), None, None, None, [product], pset)
188
+
189
+
190
+ def _style_item(file, item, color) -> None:
191
+ rgb = file.createIfcColourRgb(None, *[float(channel) for channel in color])
192
+ shading = file.createIfcSurfaceStyleShading(rgb, None)
193
+ surface = file.createIfcSurfaceStyle(None, "BOTH", [shading])
194
+ assignment = file.createIfcPresentationStyleAssignment([surface])
195
+ file.createIfcStyledItem(item, [assignment], None)
196
+
197
+
198
+ def property_value(product, name: str) -> str:
199
+ for relation in product.IsDefinedBy:
200
+ definition = relation.RelatingPropertyDefinition
201
+ if definition.is_a("IfcPropertySet") and definition.Name == "Pset_PythonCadDesign":
202
+ for prop in definition.HasProperties:
203
+ if prop.Name == name:
204
+ return str(prop.NominalValue.wrappedValue)
205
+ raise BuildExportError(f"IFC product {product.GlobalId} lacks property {name}")
206
+
207
+
208
+ def _json_safe(value: Any) -> Any:
209
+ if isinstance(value, dict):
210
+ return {str(key): _json_safe(item) for key, item in value.items()}
211
+ if isinstance(value, (list, tuple)):
212
+ return [_json_safe(item) for item in value]
213
+ if isinstance(value, (str, int, float, bool)) or value is None:
214
+ return value
215
+ return str(value)
216
+
217
+
218
+ def _normalize_header(path: Path) -> None:
219
+ text = path.read_text(encoding="utf-8")
220
+ timestamp = normalized_build_epoch().strftime("%Y-%m-%dT%H:%M:%S")
221
+ normalized, count = re.subn(
222
+ r"(FILE_NAME\s*\(\s*'[^']*'\s*,\s*')[^']*(')",
223
+ rf"\g<1>{timestamp}\2",
224
+ text,
225
+ count=1,
226
+ )
227
+ if count != 1:
228
+ raise BuildExportError("IFC header does not contain one normalizable FILE_NAME timestamp")
229
+ path.write_text(normalized, encoding="utf-8", newline="\n")
@@ -0,0 +1,136 @@
1
+ """Exact geometry-derived Python CAD Tools quantity schedules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import csv
6
+ from collections import defaultdict
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from ..determinism import write_json
11
+ from ..elements import DesignModel, QuantityRecord
12
+ from .base import selected_elements
13
+
14
+
15
+ class QuantityExporter:
16
+ name = "quantities"
17
+
18
+ def export(self, model: DesignModel, output_dir: Path) -> list[Path]:
19
+ target = output_dir / "quantities"
20
+ target.mkdir(parents=True, exist_ok=True)
21
+ records = records_for(model)
22
+ json_path = write_json(
23
+ target / "quantities.json",
24
+ {
25
+ "schema_id": "urn:python-cad-tools:schema:quantities:1",
26
+ "schema_version": 1,
27
+ "records": [record_to_dict(record) for record in records],
28
+ },
29
+ )
30
+ quantities_csv = target / "quantities.csv"
31
+ with quantities_csv.open("w", newline="", encoding="utf-8") as stream:
32
+ writer = csv.DictWriter(
33
+ stream,
34
+ fieldnames=list(record_to_dict(records[0]).keys()) if records else _fields(),
35
+ lineterminator="\n",
36
+ )
37
+ writer.writeheader()
38
+ writer.writerows(record_to_dict(record) for record in records)
39
+
40
+ materials = material_rows(model, records)
41
+ materials_csv = target / "materials.csv"
42
+ with materials_csv.open("w", newline="", encoding="utf-8") as stream:
43
+ writer = csv.DictWriter(
44
+ stream,
45
+ fieldnames=list(materials[0].keys()) if materials else _material_fields(),
46
+ lineterminator="\n",
47
+ )
48
+ writer.writeheader()
49
+ writer.writerows(materials)
50
+
51
+ summary = target / "summary.md"
52
+ total_volume = sum(record.volume_mm3 or 0 for record in records)
53
+ known_masses = [record.mass_kg for record in records if record.mass_kg is not None]
54
+ mass_line = f"{sum(known_masses):.3f} kg" if known_masses else "not calculated (no material density)"
55
+ summary.write_text(
56
+ "# Quantity Summary\n\n"
57
+ "Geometry-derived quantities from physical shared-model elements. Reference geometry is excluded.\n\n"
58
+ f"- Physical elements: {sum(record.count for record in records)}\n"
59
+ f"- Exact volume: {total_volume / 1_000_000_000:.6f} m³\n"
60
+ f"- Known mass: {mass_line}\n"
61
+ "- Waste factor: not applied\n"
62
+ "- Cost: not calculated\n",
63
+ encoding="utf-8",
64
+ )
65
+ return [json_path, quantities_csv, materials_csv, summary]
66
+
67
+
68
+ def records_for(model: DesignModel) -> list[QuantityRecord]:
69
+ result: list[QuantityRecord] = []
70
+ for element in selected_elements(model, "quantities", physical_only=True):
71
+ area = float(element.geometry.area)
72
+ volume = float(element.geometry.volume)
73
+ density = element.material.density_kg_m3 if element.material else None
74
+ mass = volume / 1_000_000_000 * density if density is not None else None
75
+ result.append(
76
+ QuantityRecord(
77
+ element_id=element.id,
78
+ category=element.category,
79
+ material_id=element.material.id if element.material else None,
80
+ count=1,
81
+ area_mm2=area,
82
+ volume_mm3=volume,
83
+ mass_kg=mass,
84
+ provenance="exact_geometry",
85
+ )
86
+ )
87
+ return sorted(result, key=lambda record: record.element_id)
88
+
89
+
90
+ def record_to_dict(record: QuantityRecord) -> dict[str, Any]:
91
+ return {
92
+ "element_id": record.element_id,
93
+ "category": record.category,
94
+ "material_id": record.material_id,
95
+ "count": record.count,
96
+ "area_mm2": round(record.area_mm2, 6) if record.area_mm2 is not None else None,
97
+ "volume_mm3": round(record.volume_mm3, 6) if record.volume_mm3 is not None else None,
98
+ "mass_kg": round(record.mass_kg, 6) if record.mass_kg is not None else None,
99
+ "provenance": record.provenance,
100
+ }
101
+
102
+
103
+ def _fields() -> list[str]:
104
+ return ["element_id", "category", "material_id", "count", "area_mm2", "volume_mm3", "mass_kg", "provenance"]
105
+
106
+
107
+ def _material_fields() -> list[str]:
108
+ return ["material_id", "material_name", "category", "element_count", "volume_m3", "mass_kg", "waste_factor"]
109
+
110
+
111
+ def material_rows(model: DesignModel, records: list[QuantityRecord]) -> list[dict[str, Any]]:
112
+ materials = {element.material.id: element.material for element in model.walk() if element.material}
113
+ grouped: dict[str, list[QuantityRecord]] = defaultdict(list)
114
+ for record in records:
115
+ if record.material_id:
116
+ grouped[record.material_id].append(record)
117
+ rows = []
118
+ for material_id in sorted(grouped):
119
+ records_for_material = grouped[material_id]
120
+ material = materials[material_id]
121
+ rows.append(
122
+ {
123
+ "material_id": material_id,
124
+ "material_name": material.name,
125
+ "category": material.category,
126
+ "element_count": sum(record.count for record in records_for_material),
127
+ "volume_m3": round(sum(record.volume_mm3 or 0 for record in records_for_material) / 1_000_000_000, 9),
128
+ "mass_kg": (
129
+ round(sum(record.mass_kg for record in records_for_material if record.mass_kg is not None), 6)
130
+ if any(record.mass_kg is not None for record in records_for_material)
131
+ else None
132
+ ),
133
+ "waste_factor": 0,
134
+ }
135
+ )
136
+ return rows
@@ -0,0 +1,117 @@
1
+ """Exact BREP STEP export with independent reload checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+
8
+ from build123d import Compound, import_step
9
+ from build123d.exporters3d import _create_xde
10
+ from OCP.IFSelect import IFSelect_ReturnStatus
11
+ from OCP.Interface import Interface_Static
12
+ from OCP.Message import Message, Message_Gravity
13
+ from OCP.STEPCAFControl import STEPCAFControl_Controller, STEPCAFControl_Writer
14
+ from OCP.STEPControl import STEPControl_Controller, STEPControl_StepModelType
15
+
16
+ from ..determinism import normalized_build_epoch, write_json
17
+ from ..exceptions import BuildExportError
18
+ from ..geometry import combined_bounds
19
+ from .base import selected_elements
20
+
21
+
22
+ class StepExporter:
23
+ name = "step"
24
+
25
+ def export(self, model, output_dir: Path) -> list[Path]:
26
+ target = output_dir / "step"
27
+ target.mkdir(parents=True, exist_ok=True)
28
+ path = target / f"{model.artifact_stem}.step"
29
+ elements = selected_elements(model, "step", physical_only=True)
30
+ if not elements:
31
+ raise BuildExportError("STEP export requires at least one physical solid")
32
+ # Initialize the controllers before setting the schema. Their first initialization
33
+ # installs defaults and would otherwise reset this value to AP214.
34
+ STEPCAFControl_Controller.Init_s()
35
+ STEPControl_Controller.Init_s()
36
+ compound = Compound(children=[element.geometry for element in elements])
37
+ _export_ap242(compound, path)
38
+ _normalize_header(path)
39
+ if not path.exists() or path.stat().st_size == 0:
40
+ raise BuildExportError("STEP writer produced no artifact")
41
+ reloaded = import_step(path)
42
+ expected = combined_bounds([element.geometry for element in elements])
43
+ actual = combined_bounds([reloaded])
44
+ if any(abs(left - right) > 0.01 for left, right in zip(expected, actual, strict=True)):
45
+ raise BuildExportError(f"STEP reload bounds differ: expected {expected}, got {actual}")
46
+ if not bool(reloaded.is_valid):
47
+ raise BuildExportError("Reloaded STEP shape is invalid")
48
+ schema = step_schema(path)
49
+ if not schema.startswith("AP242_"):
50
+ raise BuildExportError(f"STEP writer did not honor the AP242 requirement: {schema}")
51
+ report = write_json(
52
+ target / "validation.json",
53
+ {
54
+ "schema_id": "urn:python-cad-tools:schema:step-validation:1",
55
+ "schema_version": 1,
56
+ "valid": True,
57
+ "schema": schema,
58
+ "requested_schema": "AP242",
59
+ "schema_note": "The generated file was independently reloaded and its header reports AP242.",
60
+ "solid_count": len(reloaded.solids()),
61
+ "bounds_mm": [round(value, 6) for value in actual],
62
+ },
63
+ )
64
+ return [path, report]
65
+
66
+
67
+ def _export_ap242(compound: Compound, path: Path) -> None:
68
+ """Write an XDE assembly with OCP's default session so AP242 is honored."""
69
+ if not Interface_Static.SetCVal_s("write.step.schema", "AP242DIS"):
70
+ raise BuildExportError("OCP rejected the AP242 STEP schema setting")
71
+
72
+ document = _create_xde(compound, auto_naming=True)
73
+ messenger = Message.DefaultMessenger_s()
74
+ for printer in messenger.Printers():
75
+ printer.SetTraceLevel(Message_Gravity.Message_Fail)
76
+
77
+ writer = STEPCAFControl_Writer()
78
+ writer.SetColorMode(True)
79
+ writer.SetLayerMode(True)
80
+ writer.SetNameMode(True)
81
+ if not writer.Transfer(document, STEPControl_StepModelType.STEPControl_AsIs):
82
+ raise BuildExportError("STEP writer could not transfer the XDE assembly")
83
+ status = writer.Write(str(path))
84
+ if status != IFSelect_ReturnStatus.IFSelect_RetDone:
85
+ raise BuildExportError(f"STEP writer failed with status {status}")
86
+
87
+
88
+ def step_schema(path: Path) -> str:
89
+ header = path.read_text(encoding="latin-1", errors="ignore")[:16000]
90
+ match = re.search(r"FILE_SCHEMA\s*\(\s*\(\s*'([^']+)'", header)
91
+ return match.group(1) if match else "unknown"
92
+
93
+
94
+ def _normalize_header(path: Path) -> None:
95
+ text = path.read_text(encoding="latin-1")
96
+ timestamp = normalized_build_epoch().strftime("%Y-%m-%dT%H:%M:%S")
97
+ normalized, count = re.subn(
98
+ r"(FILE_NAME\s*\(\s*'[^']*'\s*,\s*')[^']*(')",
99
+ rf"\g<1>{timestamp}\2",
100
+ text,
101
+ count=1,
102
+ )
103
+ if count != 1:
104
+ raise BuildExportError("STEP header does not contain one normalizable FILE_NAME timestamp")
105
+ occurrence = 0
106
+
107
+ def normalize_occurrence(match: re.Match[str]) -> str:
108
+ nonlocal occurrence
109
+ occurrence += 1
110
+ return f"{match.group(1)}'{occurrence}'{match.group(2)}"
111
+
112
+ normalized = re.sub(
113
+ r"(NEXT_ASSEMBLY_USAGE_OCCURRENCE\s*\()'[^']*'(\s*,)",
114
+ normalize_occurrence,
115
+ normalized,
116
+ )
117
+ path.write_text(normalized, encoding="latin-1", newline="\n")
@@ -0,0 +1,187 @@
1
+ """Python CAD Tools geometry adapters using explicit millimetre inputs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from math import isfinite, sqrt
6
+ from typing import Any
7
+
8
+ from build123d import Align, Box, Edge, Face, Location, Plane, Solid, Vector, Wire
9
+
10
+ from .exceptions import ModelValidationError
11
+ from .units import Length, to_mm
12
+
13
+
14
+ def box(
15
+ length: Length,
16
+ width: Length,
17
+ height: Length,
18
+ *,
19
+ origin: tuple[Length, Length, Length] | None = None,
20
+ ) -> Solid:
21
+ _positive_lengths((length, width, height), "Box dimensions")
22
+ if origin is not None:
23
+ _point(origin, "Box origin")
24
+ shape = Box(
25
+ to_mm(length),
26
+ to_mm(width),
27
+ to_mm(height),
28
+ align=(Align.MIN, Align.MIN, Align.MIN),
29
+ )
30
+ solid = shape.solid()
31
+ if solid is None:
32
+ raise RuntimeError("build123d Box did not produce a solid")
33
+ if origin is not None:
34
+ solid = solid.moved(Location(tuple(to_mm(value) for value in origin)))
35
+ return solid
36
+
37
+
38
+ def reference_line(
39
+ start: tuple[Length, Length, Length],
40
+ end: tuple[Length, Length, Length],
41
+ ) -> Edge:
42
+ start_mm = _point(start, "Reference-line start")
43
+ end_mm = _point(end, "Reference-line end")
44
+ if start_mm == end_mm:
45
+ raise ModelValidationError("Reference-line endpoints must be distinct")
46
+ return Edge.make_line(start_mm, end_mm)
47
+
48
+
49
+ def cylinder_between(
50
+ start: tuple[Length, Length, Length],
51
+ end: tuple[Length, Length, Length],
52
+ radius: Length,
53
+ ) -> Solid:
54
+ _positive_lengths((radius,), "Cylinder radius")
55
+ start_mm = _point(start, "Cylinder start")
56
+ end_mm = _point(end, "Cylinder end")
57
+ direction = tuple(end_mm[index] - start_mm[index] for index in range(3))
58
+ height = sqrt(sum(value * value for value in direction))
59
+ if height <= 0:
60
+ raise ModelValidationError("Cylinder endpoints must be distinct")
61
+ return Solid.make_cylinder(to_mm(radius), height, Plane(origin=start_mm, z_dir=direction))
62
+
63
+
64
+ def prism_between(
65
+ start: tuple[Length, Length, Length],
66
+ end: tuple[Length, Length, Length],
67
+ width: Length,
68
+ height: Length,
69
+ ) -> Solid:
70
+ _positive_lengths((width, height), "Prism dimensions")
71
+ start_mm = _point(start, "Prism start")
72
+ end_mm = _point(end, "Prism end")
73
+ direction = tuple(end_mm[index] - start_mm[index] for index in range(3))
74
+ length = sqrt(sum(value * value for value in direction))
75
+ if length <= 0:
76
+ raise ModelValidationError("Prism endpoints must be distinct")
77
+ x_dir = tuple(value / length for value in direction)
78
+ horizontal = sqrt(x_dir[0] ** 2 + x_dir[1] ** 2)
79
+ if horizontal <= 1e-12:
80
+ z_dir = (1.0, 0.0, 0.0)
81
+ else:
82
+ z_dir = (-x_dir[0] * x_dir[2] / horizontal, -x_dir[1] * x_dir[2] / horizontal, horizontal)
83
+ return Solid.make_box(length, to_mm(width), to_mm(height), Plane(origin=start_mm, x_dir=x_dir, z_dir=z_dir))
84
+
85
+
86
+ def sloped_pool(
87
+ length: Length,
88
+ width: Length,
89
+ shallow_depth: Length,
90
+ deep_depth: Length,
91
+ *,
92
+ origin: tuple[Length, Length, Length],
93
+ ) -> Solid:
94
+ _positive_lengths((length, width, shallow_depth, deep_depth), "Sloped-pool dimensions")
95
+ if to_mm(deep_depth) < to_mm(shallow_depth):
96
+ raise ModelValidationError("Sloped-pool deep depth must be at least the shallow depth")
97
+ length_mm = to_mm(length)
98
+ width_mm = to_mm(width)
99
+ shallow_mm = to_mm(shallow_depth)
100
+ deep_mm = to_mm(deep_depth)
101
+ origin_mm = _point(origin, "Sloped-pool origin")
102
+ profile = Wire.make_polygon(
103
+ [
104
+ origin_mm,
105
+ (origin_mm[0] + length_mm, origin_mm[1], origin_mm[2]),
106
+ (origin_mm[0] + length_mm, origin_mm[1], origin_mm[2] - deep_mm),
107
+ (origin_mm[0], origin_mm[1], origin_mm[2] - shallow_mm),
108
+ ],
109
+ close=True,
110
+ )
111
+ return Solid.extrude(Face(profile), Vector(0, width_mm, 0))
112
+
113
+
114
+ def bounds(shape: Any) -> tuple[float, float, float, float, float, float]:
115
+ box_value = shape.bounding_box()
116
+ return (
117
+ float(box_value.min.X),
118
+ float(box_value.min.Y),
119
+ float(box_value.min.Z),
120
+ float(box_value.max.X),
121
+ float(box_value.max.Y),
122
+ float(box_value.max.Z),
123
+ )
124
+
125
+
126
+ def combined_bounds(shapes: list[Any]) -> tuple[float, float, float, float, float, float]:
127
+ values = [bounds(shape) for shape in shapes]
128
+ if not values:
129
+ return (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
130
+ return (
131
+ min(value[0] for value in values),
132
+ min(value[1] for value in values),
133
+ min(value[2] for value in values),
134
+ max(value[3] for value in values),
135
+ max(value[4] for value in values),
136
+ max(value[5] for value in values),
137
+ )
138
+
139
+
140
+ def tessellate(shape: Any, linear_deflection: float, angular_deflection: float) -> tuple[list[list[float]], list[list[int]]]:
141
+ vertices, triangles = shape.tessellate(linear_deflection, angular_deflection)
142
+ points = [[float(vertex.X), float(vertex.Y), float(vertex.Z)] for vertex in vertices]
143
+ faces = [[int(index) for index in triangle] for triangle in triangles]
144
+ return points, faces
145
+
146
+
147
+ def is_valid_solid(shape: Any) -> bool:
148
+ return isinstance(shape, Solid) and bool(shape.is_valid) and float(shape.volume) > 0
149
+
150
+
151
+ def diagonal(bounds_value: tuple[float, float, float, float, float, float]) -> float:
152
+ return sqrt(sum((bounds_value[index + 3] - bounds_value[index]) ** 2 for index in range(3)))
153
+
154
+
155
+ def _positive_lengths(values: tuple[Length, ...], label: str) -> None:
156
+ try:
157
+ converted = tuple(to_mm(value) for value in values)
158
+ except (TypeError, ValueError) as error:
159
+ raise ModelValidationError(f"{label} must be Length values") from error
160
+ if not all(isfinite(value) and value > 0 for value in converted):
161
+ raise ModelValidationError(f"{label} must be finite and strictly positive")
162
+
163
+
164
+ def _point(values: tuple[Length, Length, Length], label: str) -> tuple[float, float, float]:
165
+ if not isinstance(values, tuple) or len(values) != 3:
166
+ raise ModelValidationError(f"{label} must be an (x, y, z) Length tuple")
167
+ try:
168
+ converted = tuple(to_mm(value) for value in values)
169
+ except (TypeError, ValueError) as error:
170
+ raise ModelValidationError(f"{label} must be an (x, y, z) Length tuple") from error
171
+ if not all(isfinite(value) for value in converted):
172
+ raise ModelValidationError(f"{label} must contain finite coordinates")
173
+ return converted # type: ignore[return-value]
174
+
175
+
176
+ __all__ = [
177
+ "bounds",
178
+ "box",
179
+ "combined_bounds",
180
+ "cylinder_between",
181
+ "diagonal",
182
+ "is_valid_solid",
183
+ "prism_between",
184
+ "reference_line",
185
+ "sloped_pool",
186
+ "tessellate",
187
+ ]