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,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
|
+
]
|