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,124 @@
|
|
|
1
|
+
"""Public format-neutral drawing annotation contracts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
type Point2 = tuple[float, float]
|
|
9
|
+
type Direction = Literal["up", "down", "left", "right"]
|
|
10
|
+
type DrawingStyleToken = Literal[
|
|
11
|
+
"annotation-line",
|
|
12
|
+
"annotation-text",
|
|
13
|
+
"annotation-section",
|
|
14
|
+
"annotation-elevation",
|
|
15
|
+
"annotation-table",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class DrawingSheet:
|
|
21
|
+
sheet_id: str
|
|
22
|
+
view_id: Literal["plan", "front", "side", "section"]
|
|
23
|
+
origin_mm: tuple[float, float, float]
|
|
24
|
+
x_axis: tuple[float, float, float]
|
|
25
|
+
y_axis: tuple[float, float, float]
|
|
26
|
+
projected_bounds_mm: tuple[float, float, float, float]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class Line:
|
|
31
|
+
id: str
|
|
32
|
+
sheet_id: str
|
|
33
|
+
start: Point2
|
|
34
|
+
end: Point2
|
|
35
|
+
style: DrawingStyleToken
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class Polyline:
|
|
40
|
+
id: str
|
|
41
|
+
sheet_id: str
|
|
42
|
+
points: tuple[Point2, ...]
|
|
43
|
+
closed: bool
|
|
44
|
+
style: DrawingStyleToken
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True, slots=True)
|
|
48
|
+
class Text:
|
|
49
|
+
id: str
|
|
50
|
+
sheet_id: str
|
|
51
|
+
position: Point2
|
|
52
|
+
value: str
|
|
53
|
+
height_mm: float
|
|
54
|
+
rotation_degrees: float
|
|
55
|
+
style: DrawingStyleToken
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True, slots=True)
|
|
59
|
+
class SectionCallout:
|
|
60
|
+
id: str
|
|
61
|
+
sheet_id: str
|
|
62
|
+
start: Point2
|
|
63
|
+
end: Point2
|
|
64
|
+
reference_sheet_id: str
|
|
65
|
+
label: str
|
|
66
|
+
view_direction: Direction
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True, slots=True)
|
|
70
|
+
class ElevationMarker:
|
|
71
|
+
id: str
|
|
72
|
+
sheet_id: str
|
|
73
|
+
position: Point2
|
|
74
|
+
reference_sheet_id: str
|
|
75
|
+
label: str
|
|
76
|
+
direction: Direction
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True, slots=True)
|
|
80
|
+
class TableRow:
|
|
81
|
+
id: str
|
|
82
|
+
cells: tuple[str, ...]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True, slots=True)
|
|
86
|
+
class Table:
|
|
87
|
+
id: str
|
|
88
|
+
sheet_id: str
|
|
89
|
+
position: Point2
|
|
90
|
+
title: str
|
|
91
|
+
columns: tuple[str, ...]
|
|
92
|
+
rows: tuple[TableRow, ...]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
type AnnotationPrimitive = Line | Polyline | Text | SectionCallout | ElevationMarker | Table
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True, slots=True)
|
|
99
|
+
class SheetAnnotations:
|
|
100
|
+
sheet_id: str
|
|
101
|
+
annotations: tuple[AnnotationPrimitive, ...]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(frozen=True, slots=True)
|
|
105
|
+
class DrawingAnnotationSet:
|
|
106
|
+
provider_id: str
|
|
107
|
+
sheets: tuple[SheetAnnotations, ...]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
__all__ = [
|
|
111
|
+
"Direction",
|
|
112
|
+
"DrawingAnnotationSet",
|
|
113
|
+
"DrawingSheet",
|
|
114
|
+
"DrawingStyleToken",
|
|
115
|
+
"ElevationMarker",
|
|
116
|
+
"Line",
|
|
117
|
+
"Point2",
|
|
118
|
+
"Polyline",
|
|
119
|
+
"SectionCallout",
|
|
120
|
+
"SheetAnnotations",
|
|
121
|
+
"Table",
|
|
122
|
+
"TableRow",
|
|
123
|
+
"Text",
|
|
124
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Standard drawing sheet catalog and orthographic projection helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from itertools import product
|
|
6
|
+
from math import isfinite
|
|
7
|
+
from typing import Any, Literal, cast
|
|
8
|
+
|
|
9
|
+
from ..exceptions import ModelValidationError
|
|
10
|
+
from ..geometry import bounds
|
|
11
|
+
from . import DrawingSheet
|
|
12
|
+
|
|
13
|
+
_SHEET_BASES: tuple[
|
|
14
|
+
tuple[
|
|
15
|
+
str,
|
|
16
|
+
Literal["plan", "front", "side", "section"],
|
|
17
|
+
tuple[float, float, float],
|
|
18
|
+
tuple[float, float, float],
|
|
19
|
+
tuple[float, float, float],
|
|
20
|
+
],
|
|
21
|
+
...,
|
|
22
|
+
] = (
|
|
23
|
+
("A-101", "plan", (0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0)),
|
|
24
|
+
("A-201", "front", (0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 0.0, 1.0)),
|
|
25
|
+
("A-202", "side", (0.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)),
|
|
26
|
+
("A-301", "section", (0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 0.0, 1.0)),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def standard_sheet_catalog(model: Any) -> tuple[DrawingSheet, ...]:
|
|
31
|
+
physical = [
|
|
32
|
+
element
|
|
33
|
+
for element in model.walk()
|
|
34
|
+
if element.physical and element.visible and "drawings" in element.export_formats
|
|
35
|
+
]
|
|
36
|
+
if not physical:
|
|
37
|
+
raise ModelValidationError("Drawing-inclusive operations require at least one visible physical drawing element")
|
|
38
|
+
sheets = []
|
|
39
|
+
for sheet_id, view_id, origin, x_axis, y_axis in _SHEET_BASES:
|
|
40
|
+
rectangles = [project_bounds(bounds(element.geometry), origin, x_axis, y_axis) for element in physical]
|
|
41
|
+
projected = (
|
|
42
|
+
min(item[0] for item in rectangles),
|
|
43
|
+
min(item[1] for item in rectangles),
|
|
44
|
+
max(item[2] for item in rectangles),
|
|
45
|
+
max(item[3] for item in rectangles),
|
|
46
|
+
)
|
|
47
|
+
if (
|
|
48
|
+
not all(isfinite(value) for value in projected)
|
|
49
|
+
or projected[0] >= projected[2]
|
|
50
|
+
or projected[1] >= projected[3]
|
|
51
|
+
):
|
|
52
|
+
raise ModelValidationError(f"Drawing sheet {sheet_id!r} has invalid projected geometry bounds")
|
|
53
|
+
sheets.append(DrawingSheet(sheet_id, view_id, origin, x_axis, y_axis, projected))
|
|
54
|
+
return tuple(sheets)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def project_bounds(
|
|
58
|
+
bounds_mm: tuple[float, float, float, float, float, float],
|
|
59
|
+
origin: tuple[float, float, float],
|
|
60
|
+
x_axis: tuple[float, float, float],
|
|
61
|
+
y_axis: tuple[float, float, float],
|
|
62
|
+
) -> tuple[float, float, float, float]:
|
|
63
|
+
projected = []
|
|
64
|
+
for point in product(*((bounds_mm[index], bounds_mm[index + 3]) for index in range(3))):
|
|
65
|
+
relative = cast(tuple[float, float, float], tuple(point[index] - origin[index] for index in range(3)))
|
|
66
|
+
projected.append((dot(relative, x_axis), dot(relative, y_axis)))
|
|
67
|
+
return (
|
|
68
|
+
min(point[0] for point in projected),
|
|
69
|
+
min(point[1] for point in projected),
|
|
70
|
+
max(point[0] for point in projected),
|
|
71
|
+
max(point[1] for point in projected),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def dot(left: tuple[float, float, float], right: tuple[float, float, float]) -> float:
|
|
76
|
+
return sum(left[index] * right[index] for index in range(3))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
__all__: list[str] = []
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Strict validation for public neutral drawing values."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from math import isclose, isfinite
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ..exceptions import ModelValidationError
|
|
10
|
+
from . import (
|
|
11
|
+
DrawingAnnotationSet,
|
|
12
|
+
DrawingSheet,
|
|
13
|
+
ElevationMarker,
|
|
14
|
+
Line,
|
|
15
|
+
Polyline,
|
|
16
|
+
SectionCallout,
|
|
17
|
+
SheetAnnotations,
|
|
18
|
+
Table,
|
|
19
|
+
TableRow,
|
|
20
|
+
Text,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_STABLE_ID = re.compile(r"[A-Za-z0-9._-]+\Z", re.ASCII)
|
|
24
|
+
_STYLE_TOKENS = {
|
|
25
|
+
"annotation-line",
|
|
26
|
+
"annotation-text",
|
|
27
|
+
"annotation-section",
|
|
28
|
+
"annotation-elevation",
|
|
29
|
+
"annotation-table",
|
|
30
|
+
}
|
|
31
|
+
_DIRECTIONS = {"up", "down", "left", "right"}
|
|
32
|
+
_PRIMITIVE_TYPES = (Line, Polyline, Text, SectionCallout, ElevationMarker, Table)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def validate_annotation_set(value: Any, sheets: tuple[DrawingSheet, ...]) -> DrawingAnnotationSet:
|
|
36
|
+
if not isinstance(value, DrawingAnnotationSet):
|
|
37
|
+
raise ModelValidationError(
|
|
38
|
+
f"Drawing annotation provider returned {type(value).__name__}; expected DrawingAnnotationSet"
|
|
39
|
+
)
|
|
40
|
+
_stable(value.provider_id, "annotation provider ID")
|
|
41
|
+
if not isinstance(value.sheets, tuple):
|
|
42
|
+
raise ModelValidationError("DrawingAnnotationSet.sheets must be a tuple")
|
|
43
|
+
sheet_catalog = {sheet.sheet_id: sheet for sheet in sheets}
|
|
44
|
+
_validate_sheet_catalog(sheets)
|
|
45
|
+
seen_sheet_records: set[str] = set()
|
|
46
|
+
seen_ids: set[str] = set()
|
|
47
|
+
for sheet_record in value.sheets:
|
|
48
|
+
if not isinstance(sheet_record, SheetAnnotations):
|
|
49
|
+
raise ModelValidationError("DrawingAnnotationSet.sheets must contain only SheetAnnotations values")
|
|
50
|
+
if sheet_record.sheet_id not in sheet_catalog:
|
|
51
|
+
raise ModelValidationError(f"Annotations reference unknown sheet: {sheet_record.sheet_id!r}")
|
|
52
|
+
if sheet_record.sheet_id in seen_sheet_records:
|
|
53
|
+
raise ModelValidationError(f"Duplicate SheetAnnotations record: {sheet_record.sheet_id!r}")
|
|
54
|
+
seen_sheet_records.add(sheet_record.sheet_id)
|
|
55
|
+
if not isinstance(sheet_record.annotations, tuple):
|
|
56
|
+
raise ModelValidationError("SheetAnnotations.annotations must be a tuple")
|
|
57
|
+
for annotation in sheet_record.annotations:
|
|
58
|
+
if not isinstance(annotation, _PRIMITIVE_TYPES):
|
|
59
|
+
raise ModelValidationError(f"Unsupported drawing annotation primitive: {type(annotation).__name__}")
|
|
60
|
+
_validate_annotation(annotation, sheet_record.sheet_id, sheet_catalog, seen_ids)
|
|
61
|
+
for child_id in _compound_child_ids(annotation):
|
|
62
|
+
if child_id in seen_ids:
|
|
63
|
+
raise ModelValidationError(f"Duplicate annotation or compound child ID: {child_id!r}")
|
|
64
|
+
seen_ids.add(child_id)
|
|
65
|
+
return value
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _validate_sheet_catalog(sheets: tuple[DrawingSheet, ...]) -> None:
|
|
69
|
+
if not isinstance(sheets, tuple) or not sheets:
|
|
70
|
+
raise ModelValidationError("DrawingContext.sheets must be a nonempty tuple")
|
|
71
|
+
seen: set[str] = set()
|
|
72
|
+
for sheet in sheets:
|
|
73
|
+
if not isinstance(sheet, DrawingSheet):
|
|
74
|
+
raise ModelValidationError("DrawingContext.sheets contains a non-DrawingSheet value")
|
|
75
|
+
_stable(sheet.sheet_id, "drawing sheet ID")
|
|
76
|
+
if sheet.sheet_id in seen:
|
|
77
|
+
raise ModelValidationError(f"Duplicate drawing sheet ID: {sheet.sheet_id!r}")
|
|
78
|
+
seen.add(sheet.sheet_id)
|
|
79
|
+
if sheet.view_id not in {"plan", "front", "side", "section"}:
|
|
80
|
+
raise ModelValidationError(f"Unsupported drawing view ID: {sheet.view_id!r}")
|
|
81
|
+
for label, vector in (("origin", sheet.origin_mm), ("x_axis", sheet.x_axis), ("y_axis", sheet.y_axis)):
|
|
82
|
+
if not _finite_tuple(vector, 3):
|
|
83
|
+
raise ModelValidationError(f"Drawing sheet {sheet.sheet_id!r} has invalid {label}")
|
|
84
|
+
x_length = sum(value * value for value in sheet.x_axis) ** 0.5
|
|
85
|
+
y_length = sum(value * value for value in sheet.y_axis) ** 0.5
|
|
86
|
+
product = sum(left * right for left, right in zip(sheet.x_axis, sheet.y_axis, strict=True))
|
|
87
|
+
if not isclose(x_length, 1.0, abs_tol=1e-9) or not isclose(y_length, 1.0, abs_tol=1e-9):
|
|
88
|
+
raise ModelValidationError(f"Drawing sheet {sheet.sheet_id!r} axes must be unit vectors")
|
|
89
|
+
if not isclose(product, 0.0, abs_tol=1e-9):
|
|
90
|
+
raise ModelValidationError(f"Drawing sheet {sheet.sheet_id!r} axes must be orthogonal")
|
|
91
|
+
if not _finite_tuple(sheet.projected_bounds_mm, 4):
|
|
92
|
+
raise ModelValidationError(f"Drawing sheet {sheet.sheet_id!r} has invalid projected bounds")
|
|
93
|
+
min_x, min_y, max_x, max_y = sheet.projected_bounds_mm
|
|
94
|
+
if min_x >= max_x or min_y >= max_y:
|
|
95
|
+
raise ModelValidationError(f"Drawing sheet {sheet.sheet_id!r} projected bounds must have positive area")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _validate_annotation(
|
|
99
|
+
annotation: Any,
|
|
100
|
+
containing_sheet: str,
|
|
101
|
+
sheets: dict[str, DrawingSheet],
|
|
102
|
+
seen_ids: set[str],
|
|
103
|
+
) -> None:
|
|
104
|
+
_stable(annotation.id, "annotation ID")
|
|
105
|
+
if annotation.id in seen_ids:
|
|
106
|
+
raise ModelValidationError(f"Duplicate annotation ID: {annotation.id!r}")
|
|
107
|
+
seen_ids.add(annotation.id)
|
|
108
|
+
if annotation.sheet_id != containing_sheet:
|
|
109
|
+
raise ModelValidationError(
|
|
110
|
+
f"Annotation {annotation.id!r} sheet_id {annotation.sheet_id!r} does not match its container"
|
|
111
|
+
)
|
|
112
|
+
if annotation.sheet_id not in sheets:
|
|
113
|
+
raise ModelValidationError(f"Annotation {annotation.id!r} references unknown sheet {annotation.sheet_id!r}")
|
|
114
|
+
if isinstance(annotation, Line):
|
|
115
|
+
_point(annotation.start, annotation.id)
|
|
116
|
+
_point(annotation.end, annotation.id)
|
|
117
|
+
if annotation.start == annotation.end:
|
|
118
|
+
raise ModelValidationError(f"Line annotation {annotation.id!r} must be nonzero")
|
|
119
|
+
_style(annotation.style, annotation.id)
|
|
120
|
+
elif isinstance(annotation, Polyline):
|
|
121
|
+
if not isinstance(annotation.points, tuple) or len(annotation.points) < (3 if annotation.closed else 2):
|
|
122
|
+
raise ModelValidationError(f"Polyline annotation {annotation.id!r} has too few points")
|
|
123
|
+
for point in annotation.points:
|
|
124
|
+
_point(point, annotation.id)
|
|
125
|
+
if len(set(annotation.points)) < 2:
|
|
126
|
+
raise ModelValidationError(f"Polyline annotation {annotation.id!r} must contain distinct points")
|
|
127
|
+
if type(annotation.closed) is not bool:
|
|
128
|
+
raise ModelValidationError(f"Polyline annotation {annotation.id!r} closed must be bool")
|
|
129
|
+
_style(annotation.style, annotation.id)
|
|
130
|
+
elif isinstance(annotation, Text):
|
|
131
|
+
_point(annotation.position, annotation.id)
|
|
132
|
+
_text(annotation.value, annotation.id)
|
|
133
|
+
if not _finite_number(annotation.height_mm) or annotation.height_mm <= 0:
|
|
134
|
+
raise ModelValidationError(f"Text annotation {annotation.id!r} height_mm must be finite and positive")
|
|
135
|
+
if not _finite_number(annotation.rotation_degrees):
|
|
136
|
+
raise ModelValidationError(f"Text annotation {annotation.id!r} rotation_degrees must be finite")
|
|
137
|
+
_style(annotation.style, annotation.id)
|
|
138
|
+
elif isinstance(annotation, SectionCallout):
|
|
139
|
+
_point(annotation.start, annotation.id)
|
|
140
|
+
_point(annotation.end, annotation.id)
|
|
141
|
+
if annotation.start == annotation.end:
|
|
142
|
+
raise ModelValidationError(f"Section callout {annotation.id!r} must have a nonzero line")
|
|
143
|
+
_reference(annotation.reference_sheet_id, annotation.id, sheets)
|
|
144
|
+
_text(annotation.label, annotation.id)
|
|
145
|
+
_direction(annotation.view_direction, annotation.id)
|
|
146
|
+
dx = annotation.end[0] - annotation.start[0]
|
|
147
|
+
dy = annotation.end[1] - annotation.start[1]
|
|
148
|
+
if annotation.view_direction in {"left", "right"} and not isclose(dx, 0.0, abs_tol=1e-9):
|
|
149
|
+
raise ModelValidationError(f"Section callout {annotation.id!r} must be orthogonal to its view_direction")
|
|
150
|
+
if annotation.view_direction in {"up", "down"} and not isclose(dy, 0.0, abs_tol=1e-9):
|
|
151
|
+
raise ModelValidationError(f"Section callout {annotation.id!r} must be orthogonal to its view_direction")
|
|
152
|
+
elif isinstance(annotation, ElevationMarker):
|
|
153
|
+
_point(annotation.position, annotation.id)
|
|
154
|
+
_reference(annotation.reference_sheet_id, annotation.id, sheets)
|
|
155
|
+
_text(annotation.label, annotation.id)
|
|
156
|
+
_direction(annotation.direction, annotation.id)
|
|
157
|
+
elif isinstance(annotation, Table):
|
|
158
|
+
_point(annotation.position, annotation.id)
|
|
159
|
+
_text(annotation.title, annotation.id)
|
|
160
|
+
if not isinstance(annotation.columns, tuple) or not annotation.columns:
|
|
161
|
+
raise ModelValidationError(f"Table annotation {annotation.id!r} must have columns")
|
|
162
|
+
for column in annotation.columns:
|
|
163
|
+
_text(column, annotation.id)
|
|
164
|
+
if not isinstance(annotation.rows, tuple):
|
|
165
|
+
raise ModelValidationError(f"Table annotation {annotation.id!r} rows must be a tuple")
|
|
166
|
+
row_ids: set[str] = set()
|
|
167
|
+
for row in annotation.rows:
|
|
168
|
+
if not isinstance(row, TableRow):
|
|
169
|
+
raise ModelValidationError(f"Table annotation {annotation.id!r} contains a non-TableRow value")
|
|
170
|
+
_stable(row.id, f"row ID in table {annotation.id!r}")
|
|
171
|
+
if row.id in row_ids:
|
|
172
|
+
raise ModelValidationError(f"Duplicate drawing table row ID: {row.id!r}")
|
|
173
|
+
row_ids.add(row.id)
|
|
174
|
+
if not isinstance(row.cells, tuple) or len(row.cells) != len(annotation.columns):
|
|
175
|
+
raise ModelValidationError(f"Table row {row.id!r} cell count does not match its columns")
|
|
176
|
+
for cell in row.cells:
|
|
177
|
+
_text(cell, row.id)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _stable(value: object, label: str) -> None:
|
|
181
|
+
if not isinstance(value, str) or _STABLE_ID.fullmatch(value) is None:
|
|
182
|
+
raise ModelValidationError(f"{label} must match [A-Za-z0-9._-]+")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _style(value: object, annotation_id: str) -> None:
|
|
186
|
+
if value not in _STYLE_TOKENS:
|
|
187
|
+
raise ModelValidationError(f"Annotation {annotation_id!r} has unsupported style token {value!r}")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _direction(value: object, annotation_id: str) -> None:
|
|
191
|
+
if value not in _DIRECTIONS:
|
|
192
|
+
raise ModelValidationError(f"Annotation {annotation_id!r} has unsupported direction {value!r}")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _reference(value: object, annotation_id: str, sheets: dict[str, DrawingSheet]) -> None:
|
|
196
|
+
if not isinstance(value, str) or value not in sheets:
|
|
197
|
+
raise ModelValidationError(f"Annotation {annotation_id!r} references unknown sheet {value!r}")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _point(value: object, annotation_id: str) -> None:
|
|
201
|
+
if not _finite_tuple(value, 2):
|
|
202
|
+
raise ModelValidationError(f"Annotation {annotation_id!r} contains an invalid point")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _text(value: object, annotation_id: str) -> None:
|
|
206
|
+
if (
|
|
207
|
+
not isinstance(value, str)
|
|
208
|
+
or not value.strip()
|
|
209
|
+
or any(ord(character) < 32 or ord(character) == 127 for character in value)
|
|
210
|
+
):
|
|
211
|
+
raise ModelValidationError(f"Annotation {annotation_id!r} contains invalid text")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _finite_tuple(value: object, length: int) -> bool:
|
|
215
|
+
return isinstance(value, tuple) and len(value) == length and all(_finite_number(item) for item in value)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _finite_number(value: object) -> bool:
|
|
219
|
+
return isinstance(value, (int, float)) and not isinstance(value, bool) and isfinite(value)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _compound_child_ids(annotation: Any) -> tuple[str, ...]:
|
|
223
|
+
if isinstance(annotation, SectionCallout):
|
|
224
|
+
suffixes = ("line", "arrow.start", "arrow.end", "label.start", "label.end")
|
|
225
|
+
return tuple(f"{annotation.id}.{suffix}" for suffix in suffixes)
|
|
226
|
+
if isinstance(annotation, ElevationMarker):
|
|
227
|
+
return tuple(f"{annotation.id}.{suffix}" for suffix in ("outline", "pointer", "label"))
|
|
228
|
+
if isinstance(annotation, Table):
|
|
229
|
+
ids = [f"{annotation.id}.title", f"{annotation.id}.border"]
|
|
230
|
+
ids.extend(f"{annotation.id}.header.{index}" for index in range(len(annotation.columns)))
|
|
231
|
+
for row in annotation.rows:
|
|
232
|
+
ids.extend(f"{annotation.id}.row.{row.id}.cell.{index}" for index in range(len(annotation.columns)))
|
|
233
|
+
ids.append(f"{annotation.id}.row.{row.id}.separator")
|
|
234
|
+
return tuple(ids)
|
|
235
|
+
return ()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
__all__: list[str] = []
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Typed intermediate model shared by Python CAD Tools exporters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
Color = tuple[float, float, float]
|
|
10
|
+
GeometryKind = Literal["solid", "shell", "wire", "edge", "reference"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class MaterialSpec:
|
|
15
|
+
id: str
|
|
16
|
+
name: str
|
|
17
|
+
category: str
|
|
18
|
+
color_rgb: Color
|
|
19
|
+
density_kg_m3: float | None = None
|
|
20
|
+
properties: dict[str, Any] = field(default_factory=dict)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class Placement:
|
|
25
|
+
translation_mm: tuple[float, float, float] = (0.0, 0.0, 0.0)
|
|
26
|
+
rotation_deg_xyz: tuple[float, float, float] = (0.0, 0.0, 0.0)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class Dimensions:
|
|
31
|
+
length_mm: float | None = None
|
|
32
|
+
width_mm: float | None = None
|
|
33
|
+
height_mm: float | None = None
|
|
34
|
+
radius_mm: float | None = None
|
|
35
|
+
extras: dict[str, float] = field(default_factory=dict)
|
|
36
|
+
|
|
37
|
+
def values(self) -> tuple[float, ...]:
|
|
38
|
+
primary = (self.length_mm, self.width_mm, self.height_mm, self.radius_mm)
|
|
39
|
+
return tuple(value for value in primary if value is not None) + tuple(
|
|
40
|
+
self.extras[key] for key in sorted(self.extras)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True, slots=True)
|
|
45
|
+
class IfcMapping:
|
|
46
|
+
ifc_class: str | None
|
|
47
|
+
predefined_type: str | None = None
|
|
48
|
+
exclude_reason: str | None = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(slots=True)
|
|
52
|
+
class DesignElement:
|
|
53
|
+
id: str
|
|
54
|
+
name: str
|
|
55
|
+
category: str
|
|
56
|
+
geometry: Any
|
|
57
|
+
geometry_kind: GeometryKind
|
|
58
|
+
placement: Placement = field(default_factory=Placement)
|
|
59
|
+
dimensions: Dimensions = field(default_factory=Dimensions)
|
|
60
|
+
material: MaterialSpec | None = None
|
|
61
|
+
color_rgb: Color | None = None
|
|
62
|
+
ifc_mapping: IfcMapping = field(default_factory=lambda: IfcMapping(None, exclude_reason="not mapped"))
|
|
63
|
+
storey: str | None = None
|
|
64
|
+
tags: set[str] = field(default_factory=set)
|
|
65
|
+
properties: dict[str, Any] = field(default_factory=dict)
|
|
66
|
+
children: list[DesignElement] = field(default_factory=list)
|
|
67
|
+
visible: bool = True
|
|
68
|
+
export_formats: set[str] = field(default_factory=lambda: {"step", "ifc", "glb", "drawings", "quantities"})
|
|
69
|
+
source_module: str = "model"
|
|
70
|
+
physical: bool = True
|
|
71
|
+
parent_id: str | None = None
|
|
72
|
+
|
|
73
|
+
def walk(self) -> Iterator[DesignElement]:
|
|
74
|
+
yield self
|
|
75
|
+
for child in sorted(self.children, key=lambda item: item.id):
|
|
76
|
+
yield from child.walk()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(slots=True)
|
|
80
|
+
class DesignModel:
|
|
81
|
+
id: str
|
|
82
|
+
name: str
|
|
83
|
+
artifact_stem: str
|
|
84
|
+
elements: list[DesignElement]
|
|
85
|
+
coordinate_system: str = "right-handed; X right, Y depth/away, Z up; millimetres"
|
|
86
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
87
|
+
validators: list[Any] = field(default_factory=list)
|
|
88
|
+
|
|
89
|
+
def walk(self) -> Iterator[DesignElement]:
|
|
90
|
+
for element in sorted(self.elements, key=lambda item: item.id):
|
|
91
|
+
yield from element.walk()
|
|
92
|
+
|
|
93
|
+
def get(self, stable_id: str) -> DesignElement:
|
|
94
|
+
for element in self.walk():
|
|
95
|
+
if element.id == stable_id:
|
|
96
|
+
return element
|
|
97
|
+
raise KeyError(stable_id)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True, slots=True)
|
|
101
|
+
class QuantityRecord:
|
|
102
|
+
element_id: str
|
|
103
|
+
category: str
|
|
104
|
+
material_id: str | None
|
|
105
|
+
count: int
|
|
106
|
+
area_mm2: float | None
|
|
107
|
+
volume_mm3: float | None
|
|
108
|
+
mass_kg: float | None
|
|
109
|
+
provenance: str
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
__all__ = [
|
|
113
|
+
"Color",
|
|
114
|
+
"DesignElement",
|
|
115
|
+
"DesignModel",
|
|
116
|
+
"Dimensions",
|
|
117
|
+
"GeometryKind",
|
|
118
|
+
"IfcMapping",
|
|
119
|
+
"MaterialSpec",
|
|
120
|
+
"Placement",
|
|
121
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Public exception hierarchy for Python CAD Tools."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CadToolsError(RuntimeError):
|
|
5
|
+
"""Base class for actionable Python CAD Tools failures."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ProjectConfigError(CadToolsError):
|
|
9
|
+
"""Project configuration or entry-point loading is invalid."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ModelValidationError(CadToolsError):
|
|
13
|
+
"""The project model violates a public model contract."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BuildExportError(CadToolsError):
|
|
17
|
+
"""An exporter failed to create or validate an artifact."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ArtifactIntegrityError(CadToolsError):
|
|
21
|
+
"""Generated artifact bytes do not match their inventory."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class UnsafePathError(CadToolsError):
|
|
25
|
+
"""A configured or requested path escapes the project boundary."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BuildLockedError(CadToolsError):
|
|
29
|
+
"""A project operation cannot run because execution is already active."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SiteError(CadToolsError):
|
|
33
|
+
"""Viewer-site preparation or serving failed."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"ArtifactIntegrityError",
|
|
38
|
+
"BuildExportError",
|
|
39
|
+
"BuildLockedError",
|
|
40
|
+
"CadToolsError",
|
|
41
|
+
"ModelValidationError",
|
|
42
|
+
"ProjectConfigError",
|
|
43
|
+
"SiteError",
|
|
44
|
+
"UnsafePathError",
|
|
45
|
+
]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Format exporters for the shared Python CAD Tools design model."""
|
|
2
|
+
|
|
3
|
+
from .glb import GlbExporter
|
|
4
|
+
from .ifc import IfcExporter
|
|
5
|
+
from .quantities import QuantityExporter
|
|
6
|
+
from .step import StepExporter
|
|
7
|
+
|
|
8
|
+
__all__ = ["GlbExporter", "IfcExporter", "QuantityExporter", "StepExporter"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Common Python CAD Tools exporter protocol and helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
|
|
8
|
+
from ..elements import DesignElement, DesignModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Exporter(Protocol):
|
|
12
|
+
name: str
|
|
13
|
+
|
|
14
|
+
def export(self, model: DesignModel, output_dir: Path) -> list[Path]: ...
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def selected_elements(model: DesignModel, format_name: str, *, physical_only: bool = False) -> list[DesignElement]:
|
|
18
|
+
return [
|
|
19
|
+
element
|
|
20
|
+
for element in model.walk()
|
|
21
|
+
if element.visible
|
|
22
|
+
and format_name in element.export_formats
|
|
23
|
+
and (element.physical or not physical_only)
|
|
24
|
+
]
|