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,119 @@
|
|
|
1
|
+
"""Python CAD Tools vector PDF sheet-set writer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from reportlab.lib.pagesizes import landscape, letter
|
|
8
|
+
from reportlab.pdfgen.canvas import Canvas
|
|
9
|
+
|
|
10
|
+
from .primitives import DrawingScene
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def write_pdf(scenes: list[DrawingScene], path: Path) -> Path:
|
|
14
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
15
|
+
page_size = landscape(letter)
|
|
16
|
+
canvas = Canvas(str(path), pagesize=page_size, pageCompression=0, invariant=1)
|
|
17
|
+
for scene in scenes:
|
|
18
|
+
_draw_scene(canvas, scene, page_size)
|
|
19
|
+
canvas.showPage()
|
|
20
|
+
canvas.save()
|
|
21
|
+
return path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _draw_scene(canvas: Canvas, scene: DrawingScene, page_size: tuple[float, float]) -> None:
|
|
25
|
+
page_width, page_height = page_size
|
|
26
|
+
margin = 42
|
|
27
|
+
canvas.setFillColorRGB(1, 1, 1)
|
|
28
|
+
canvas.rect(0, 0, page_width, page_height, stroke=0, fill=1)
|
|
29
|
+
canvas.setFillColorRGB(0, 0, 0)
|
|
30
|
+
canvas.setStrokeColorRGB(0.2, 0.2, 0.2)
|
|
31
|
+
canvas.setLineWidth(0.6)
|
|
32
|
+
canvas.rect(24, 24, page_width - 48, page_height - 48, stroke=1, fill=0)
|
|
33
|
+
x0, y0, x1, y1 = scene.viewport.bounds
|
|
34
|
+
scale = min((page_width - 2 * margin) / (x1 - x0), (page_height - 3 * margin) / (y1 - y0))
|
|
35
|
+
|
|
36
|
+
def point(value: tuple[float, float]) -> tuple[float, float]:
|
|
37
|
+
return margin + (value[0] - x0) * scale, margin * 1.5 + (value[1] - y0) * scale
|
|
38
|
+
|
|
39
|
+
canvas.setStrokeColorRGB(0.05, 0.05, 0.05)
|
|
40
|
+
canvas.setLineWidth(1.0)
|
|
41
|
+
for polyline in scene.polylines:
|
|
42
|
+
path = canvas.beginPath()
|
|
43
|
+
start = point(polyline.points[0])
|
|
44
|
+
path.moveTo(*start)
|
|
45
|
+
for source in polyline.points[1:]:
|
|
46
|
+
path.lineTo(*point(source))
|
|
47
|
+
if polyline.closed:
|
|
48
|
+
path.close()
|
|
49
|
+
canvas.drawPath(path)
|
|
50
|
+
canvas.saveState()
|
|
51
|
+
canvas.setStrokeColorRGB(0.1, 0.35, 0.55)
|
|
52
|
+
canvas.setDash(7, 3)
|
|
53
|
+
canvas.setLineWidth(0.8)
|
|
54
|
+
for line in scene.lines:
|
|
55
|
+
canvas.line(*point(line.start), *point(line.end))
|
|
56
|
+
canvas.restoreState()
|
|
57
|
+
canvas.saveState()
|
|
58
|
+
canvas.setStrokeColorRGB(0.25, 0.25, 0.25)
|
|
59
|
+
canvas.setFillColorRGB(0.15, 0.15, 0.15)
|
|
60
|
+
canvas.setLineWidth(0.65)
|
|
61
|
+
canvas.setFont("Helvetica", 7)
|
|
62
|
+
for dimension in scene.dimensions:
|
|
63
|
+
dx = dimension.end[0] - dimension.start[0]
|
|
64
|
+
dy = dimension.end[1] - dimension.start[1]
|
|
65
|
+
if abs(dx) >= abs(dy):
|
|
66
|
+
start = (dimension.start[0], dimension.start[1] + dimension.offset)
|
|
67
|
+
end = (dimension.end[0], dimension.end[1] + dimension.offset)
|
|
68
|
+
else:
|
|
69
|
+
start = (dimension.start[0] + dimension.offset, dimension.start[1])
|
|
70
|
+
end = (dimension.end[0] + dimension.offset, dimension.end[1])
|
|
71
|
+
start_page = point(start)
|
|
72
|
+
end_page = point(end)
|
|
73
|
+
canvas.line(*start_page, *end_page)
|
|
74
|
+
canvas.line(start_page[0] - 3, start_page[1] - 3, start_page[0] + 3, start_page[1] + 3)
|
|
75
|
+
canvas.line(end_page[0] - 3, end_page[1] - 3, end_page[0] + 3, end_page[1] + 3)
|
|
76
|
+
canvas.drawCentredString(
|
|
77
|
+
(start_page[0] + end_page[0]) / 2,
|
|
78
|
+
(start_page[1] + end_page[1]) / 2 + 4,
|
|
79
|
+
dimension.label,
|
|
80
|
+
)
|
|
81
|
+
canvas.restoreState()
|
|
82
|
+
canvas.setFont("Helvetica", 7)
|
|
83
|
+
for text in scene.texts:
|
|
84
|
+
canvas.drawString(*point(text.position), text.value)
|
|
85
|
+
for entity in scene.annotation_entities:
|
|
86
|
+
canvas.saveState()
|
|
87
|
+
canvas.setStrokeColorRGB(0.48, 0.12, 0.12)
|
|
88
|
+
canvas.setFillColorRGB(0.48, 0.12, 0.12)
|
|
89
|
+
canvas.setLineWidth(0.8)
|
|
90
|
+
if entity.kind == "line":
|
|
91
|
+
canvas.line(*point(entity.points[0]), *point(entity.points[1]))
|
|
92
|
+
elif entity.kind == "polyline":
|
|
93
|
+
annotation_path = canvas.beginPath()
|
|
94
|
+
annotation_path.moveTo(*point(entity.points[0]))
|
|
95
|
+
for source in entity.points[1:]:
|
|
96
|
+
annotation_path.lineTo(*point(source))
|
|
97
|
+
if entity.closed:
|
|
98
|
+
annotation_path.close()
|
|
99
|
+
canvas.drawPath(annotation_path)
|
|
100
|
+
elif entity.kind == "circle" and entity.center is not None and entity.radius is not None:
|
|
101
|
+
center = point(entity.center)
|
|
102
|
+
canvas.circle(center[0], center[1], entity.radius * scale, stroke=1, fill=0)
|
|
103
|
+
elif entity.kind == "text" and entity.text is not None:
|
|
104
|
+
x, y = point(entity.points[0])
|
|
105
|
+
canvas.translate(x, y)
|
|
106
|
+
canvas.rotate(entity.rotation_degrees)
|
|
107
|
+
canvas.setFont("Helvetica", max(entity.height_mm * scale, 5.0))
|
|
108
|
+
canvas.drawString(0, 0, entity.text)
|
|
109
|
+
canvas.restoreState()
|
|
110
|
+
canvas.setFont("Helvetica-Bold", 12)
|
|
111
|
+
canvas.drawString(margin, page_height - margin, scene.title_block.sheet_title)
|
|
112
|
+
canvas.setFont("Helvetica", 8)
|
|
113
|
+
canvas.drawString(margin, margin * 0.72, scene.title_block.project_name)
|
|
114
|
+
canvas.drawRightString(page_width - margin, page_height - margin, f"{scene.title_block.sheet_number} Rev {scene.title_block.revision}")
|
|
115
|
+
canvas.line(margin, margin * 1.05, page_width - margin, margin * 1.05)
|
|
116
|
+
canvas.setFillColorRGB(0.65, 0.0, 0.0)
|
|
117
|
+
canvas.setFont("Helvetica-Bold", 11)
|
|
118
|
+
canvas.drawCentredString(page_width / 2, margin * 0.72, scene.title_block.notice)
|
|
119
|
+
canvas.setFillColorRGB(0, 0, 0)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Format-neutral Python CAD Tools drawing primitives."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from .annotations import AnnotationEntity
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class Line:
|
|
14
|
+
start: tuple[float, float]
|
|
15
|
+
end: tuple[float, float]
|
|
16
|
+
layer: str = "VISIBLE"
|
|
17
|
+
source_id: str | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class Polyline:
|
|
22
|
+
points: tuple[tuple[float, float], ...]
|
|
23
|
+
closed: bool = False
|
|
24
|
+
layer: str = "VISIBLE"
|
|
25
|
+
source_id: str | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True, slots=True)
|
|
29
|
+
class Arc:
|
|
30
|
+
center: tuple[float, float]
|
|
31
|
+
radius: float
|
|
32
|
+
start_deg: float
|
|
33
|
+
end_deg: float
|
|
34
|
+
layer: str = "VISIBLE"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class Text:
|
|
39
|
+
position: tuple[float, float]
|
|
40
|
+
value: str
|
|
41
|
+
height: float = 3.0
|
|
42
|
+
layer: str = "TEXT"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True, slots=True)
|
|
46
|
+
class Dimension:
|
|
47
|
+
start: tuple[float, float]
|
|
48
|
+
end: tuple[float, float]
|
|
49
|
+
offset: float
|
|
50
|
+
label: str
|
|
51
|
+
source_id: str | None = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True, slots=True)
|
|
55
|
+
class Leader:
|
|
56
|
+
points: tuple[tuple[float, float], ...]
|
|
57
|
+
label: str
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True, slots=True)
|
|
61
|
+
class Hatch:
|
|
62
|
+
boundary: tuple[tuple[float, float], ...]
|
|
63
|
+
pattern: str = "ANSI31"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True, slots=True)
|
|
67
|
+
class Viewport:
|
|
68
|
+
name: str
|
|
69
|
+
bounds: tuple[float, float, float, float]
|
|
70
|
+
scale: float
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True, slots=True)
|
|
74
|
+
class TitleBlock:
|
|
75
|
+
project_name: str
|
|
76
|
+
sheet_title: str
|
|
77
|
+
sheet_number: str
|
|
78
|
+
revision: str
|
|
79
|
+
notice: str = "Conceptual — not for construction or permitting"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(slots=True)
|
|
83
|
+
class DrawingScene:
|
|
84
|
+
title_block: TitleBlock
|
|
85
|
+
viewport: Viewport
|
|
86
|
+
lines: list[Line] = field(default_factory=list)
|
|
87
|
+
polylines: list[Polyline] = field(default_factory=list)
|
|
88
|
+
arcs: list[Arc] = field(default_factory=list)
|
|
89
|
+
texts: list[Text] = field(default_factory=list)
|
|
90
|
+
dimensions: list[Dimension] = field(default_factory=list)
|
|
91
|
+
leaders: list[Leader] = field(default_factory=list)
|
|
92
|
+
hatches: list[Hatch] = field(default_factory=list)
|
|
93
|
+
annotation_entities: tuple[AnnotationEntity, ...] = ()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Orthographic shared-model projection into Python CAD Tools primitives."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ...drawings import AnnotationPrimitive, DrawingSheet
|
|
6
|
+
from ...drawings._catalog import project_bounds
|
|
7
|
+
from ...geometry import bounds
|
|
8
|
+
from ...units import format_feet_inches
|
|
9
|
+
from ..base import selected_elements
|
|
10
|
+
from .annotations import annotation_bounds, flatten_annotations
|
|
11
|
+
from .primitives import Dimension, DrawingScene, Line, Polyline, Text, TitleBlock, Viewport
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def project_model(
|
|
15
|
+
model,
|
|
16
|
+
sheet: DrawingSheet,
|
|
17
|
+
annotations: tuple[AnnotationPrimitive, ...] = (),
|
|
18
|
+
revision: str = "WORKING",
|
|
19
|
+
) -> DrawingScene:
|
|
20
|
+
physical = selected_elements(model, "drawings", physical_only=True)
|
|
21
|
+
rectangles = []
|
|
22
|
+
for element in physical:
|
|
23
|
+
rectangle = project_bounds(bounds(element.geometry), sheet.origin_mm, sheet.x_axis, sheet.y_axis)
|
|
24
|
+
rectangles.append((element, rectangle))
|
|
25
|
+
geometry_bounds = sheet.projected_bounds_mm
|
|
26
|
+
min_x, min_y, max_x, max_y = geometry_bounds
|
|
27
|
+
annotation_entities = flatten_annotations(annotations)
|
|
28
|
+
annotation_rectangle = annotation_bounds(annotation_entities)
|
|
29
|
+
if annotation_rectangle is not None:
|
|
30
|
+
min_x = min(min_x, annotation_rectangle[0])
|
|
31
|
+
min_y = min(min_y, annotation_rectangle[1])
|
|
32
|
+
max_x = max(max_x, annotation_rectangle[2])
|
|
33
|
+
max_y = max(max_y, annotation_rectangle[3])
|
|
34
|
+
padding = max(max_x - min_x, max_y - min_y) * 0.12
|
|
35
|
+
scene = DrawingScene(
|
|
36
|
+
title_block=TitleBlock(model.name, f"Overall {sheet.view_id.title()}", sheet.sheet_id, revision),
|
|
37
|
+
viewport=Viewport(sheet.view_id, (min_x - padding, min_y - padding, max_x + padding, max_y + padding), 1.0),
|
|
38
|
+
annotation_entities=annotation_entities,
|
|
39
|
+
)
|
|
40
|
+
label_all = len(rectangles) <= 20
|
|
41
|
+
for element, (x0, y0, x1, y1) in rectangles:
|
|
42
|
+
scene.polylines.append(
|
|
43
|
+
Polyline(((x0, y0), (x1, y0), (x1, y1), (x0, y1)), True, "VISIBLE", element.id)
|
|
44
|
+
)
|
|
45
|
+
if label_all or element.properties.get("drawing_label", False):
|
|
46
|
+
scene.texts.append(
|
|
47
|
+
Text((x0, y1 + padding * 0.12), f"{element.name} [{element.id}]", max(padding * 0.035, 3))
|
|
48
|
+
)
|
|
49
|
+
for element in selected_elements(model, "drawings"):
|
|
50
|
+
if element.physical:
|
|
51
|
+
continue
|
|
52
|
+
projected = project_bounds(bounds(element.geometry), sheet.origin_mm, sheet.x_axis, sheet.y_axis)
|
|
53
|
+
start, end = (projected[0], projected[1]), (projected[2], projected[3])
|
|
54
|
+
if start != end:
|
|
55
|
+
scene.lines.append(Line(start, end, "REFERENCE", element.id))
|
|
56
|
+
if rectangles:
|
|
57
|
+
x0, y0, x1, y1 = geometry_bounds
|
|
58
|
+
scene.dimensions.extend(
|
|
59
|
+
[
|
|
60
|
+
Dimension((x0, y0), (x1, y0), -padding * 0.35, format_feet_inches(x1 - x0), model.id),
|
|
61
|
+
Dimension((x0, y0), (x0, y1), -padding * 0.35, format_feet_inches(y1 - y0), model.id),
|
|
62
|
+
]
|
|
63
|
+
)
|
|
64
|
+
return scene
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""SVG writer for the shared Python CAD Tools drawing scene."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from xml.etree import ElementTree as ET
|
|
7
|
+
|
|
8
|
+
from .primitives import DrawingScene
|
|
9
|
+
|
|
10
|
+
SVG_NS = "http://www.w3.org/2000/svg"
|
|
11
|
+
ET.register_namespace("", SVG_NS)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def write_svg(scene: DrawingScene, path: Path) -> Path:
|
|
15
|
+
x0, y0, x1, y1 = scene.viewport.bounds
|
|
16
|
+
width = x1 - x0
|
|
17
|
+
height = y1 - y0
|
|
18
|
+
root = ET.Element(
|
|
19
|
+
f"{{{SVG_NS}}}svg",
|
|
20
|
+
{
|
|
21
|
+
"viewBox": f"{x0:g} {y0:g} {width:g} {height:g}",
|
|
22
|
+
"data-sheet": scene.title_block.sheet_number,
|
|
23
|
+
},
|
|
24
|
+
)
|
|
25
|
+
ET.SubElement(root, f"{{{SVG_NS}}}title").text = scene.title_block.sheet_title
|
|
26
|
+
group = ET.SubElement(root, f"{{{SVG_NS}}}g", {"transform": f"translate(0 {y0 + y1:g}) scale(1 -1)"})
|
|
27
|
+
for polyline in scene.polylines:
|
|
28
|
+
points = " ".join(f"{x:g},{y:g}" for x, y in polyline.points)
|
|
29
|
+
ET.SubElement(
|
|
30
|
+
group,
|
|
31
|
+
f"{{{SVG_NS}}}{'polygon' if polyline.closed else 'polyline'}",
|
|
32
|
+
{
|
|
33
|
+
"points": points,
|
|
34
|
+
"fill": "none",
|
|
35
|
+
"stroke": "#111111",
|
|
36
|
+
"stroke-width": "2",
|
|
37
|
+
"vector-effect": "non-scaling-stroke",
|
|
38
|
+
"data-layer": polyline.layer,
|
|
39
|
+
"data-source-id": polyline.source_id or "",
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
for line in scene.lines:
|
|
43
|
+
ET.SubElement(
|
|
44
|
+
group,
|
|
45
|
+
f"{{{SVG_NS}}}line",
|
|
46
|
+
{
|
|
47
|
+
"x1": f"{line.start[0]:g}", "y1": f"{line.start[1]:g}",
|
|
48
|
+
"x2": f"{line.end[0]:g}", "y2": f"{line.end[1]:g}",
|
|
49
|
+
"stroke": "#111111", "data-layer": line.layer,
|
|
50
|
+
},
|
|
51
|
+
)
|
|
52
|
+
text_group = ET.SubElement(root, f"{{{SVG_NS}}}g", {"fill": "#111111", "font-family": "sans-serif"})
|
|
53
|
+
for item in scene.texts:
|
|
54
|
+
ET.SubElement(
|
|
55
|
+
text_group,
|
|
56
|
+
f"{{{SVG_NS}}}text",
|
|
57
|
+
{"x": f"{item.position[0]:g}", "y": f"{y0 + y1 - item.position[1]:g}", "font-size": f"{item.height:g}"},
|
|
58
|
+
).text = item.value
|
|
59
|
+
for dimension in scene.dimensions:
|
|
60
|
+
dx = dimension.end[0] - dimension.start[0]
|
|
61
|
+
dy = dimension.end[1] - dimension.start[1]
|
|
62
|
+
if abs(dx) >= abs(dy):
|
|
63
|
+
a = (dimension.start[0], dimension.start[1] + dimension.offset)
|
|
64
|
+
b = (dimension.end[0], dimension.end[1] + dimension.offset)
|
|
65
|
+
else:
|
|
66
|
+
a = (dimension.start[0] + dimension.offset, dimension.start[1])
|
|
67
|
+
b = (dimension.end[0] + dimension.offset, dimension.end[1])
|
|
68
|
+
ET.SubElement(group, f"{{{SVG_NS}}}line", {"x1": f"{a[0]:g}", "y1": f"{a[1]:g}", "x2": f"{b[0]:g}", "y2": f"{b[1]:g}", "stroke": "#333333", "data-layer": "DIMENSIONS"})
|
|
69
|
+
ET.SubElement(text_group, f"{{{SVG_NS}}}text", {"x": f"{(a[0] + b[0]) / 2:g}", "y": f"{y0 + y1 - (a[1] + b[1]) / 2:g}", "font-size": "12", "text-anchor": "middle"}).text = dimension.label
|
|
70
|
+
annotation_group = ET.SubElement(group, f"{{{SVG_NS}}}g", {"data-layer": "ANNOTATIONS"})
|
|
71
|
+
annotation_text_group = ET.SubElement(
|
|
72
|
+
root,
|
|
73
|
+
f"{{{SVG_NS}}}g",
|
|
74
|
+
{"data-layer": "ANNOTATIONS", "fill": "#111111", "font-family": "sans-serif"},
|
|
75
|
+
)
|
|
76
|
+
for entity in scene.annotation_entities:
|
|
77
|
+
attributes = {"data-annotation-id": entity.id, "data-style": entity.style}
|
|
78
|
+
if entity.kind == "line":
|
|
79
|
+
start, end = entity.points
|
|
80
|
+
ET.SubElement(
|
|
81
|
+
annotation_group,
|
|
82
|
+
f"{{{SVG_NS}}}line",
|
|
83
|
+
attributes
|
|
84
|
+
| {
|
|
85
|
+
"x1": f"{start[0]:g}",
|
|
86
|
+
"y1": f"{start[1]:g}",
|
|
87
|
+
"x2": f"{end[0]:g}",
|
|
88
|
+
"y2": f"{end[1]:g}",
|
|
89
|
+
"fill": "none",
|
|
90
|
+
"stroke": "#7a1f1f",
|
|
91
|
+
"vector-effect": "non-scaling-stroke",
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
elif entity.kind == "polyline":
|
|
95
|
+
ET.SubElement(
|
|
96
|
+
annotation_group,
|
|
97
|
+
f"{{{SVG_NS}}}{'polygon' if entity.closed else 'polyline'}",
|
|
98
|
+
attributes
|
|
99
|
+
| {
|
|
100
|
+
"points": " ".join(f"{x:g},{y:g}" for x, y in entity.points),
|
|
101
|
+
"fill": "none",
|
|
102
|
+
"stroke": "#7a1f1f",
|
|
103
|
+
"vector-effect": "non-scaling-stroke",
|
|
104
|
+
},
|
|
105
|
+
)
|
|
106
|
+
elif entity.kind == "circle" and entity.center is not None and entity.radius is not None:
|
|
107
|
+
ET.SubElement(
|
|
108
|
+
annotation_group,
|
|
109
|
+
f"{{{SVG_NS}}}circle",
|
|
110
|
+
attributes
|
|
111
|
+
| {
|
|
112
|
+
"cx": f"{entity.center[0]:g}",
|
|
113
|
+
"cy": f"{entity.center[1]:g}",
|
|
114
|
+
"r": f"{entity.radius:g}",
|
|
115
|
+
"fill": "none",
|
|
116
|
+
"stroke": "#7a1f1f",
|
|
117
|
+
"vector-effect": "non-scaling-stroke",
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
elif entity.kind == "text" and entity.text is not None:
|
|
121
|
+
x, y = entity.points[0]
|
|
122
|
+
text_attributes = attributes | {
|
|
123
|
+
"x": f"{x:g}",
|
|
124
|
+
"y": f"{y0 + y1 - y:g}",
|
|
125
|
+
"font-size": f"{entity.height_mm:g}",
|
|
126
|
+
}
|
|
127
|
+
if entity.rotation_degrees:
|
|
128
|
+
text_attributes["transform"] = f"rotate({-entity.rotation_degrees:g} {x:g} {y0 + y1 - y:g})"
|
|
129
|
+
ET.SubElement(annotation_text_group, f"{{{SVG_NS}}}text", text_attributes).text = entity.text
|
|
130
|
+
notice = ET.SubElement(root, f"{{{SVG_NS}}}text", {"x": f"{x0 + width * 0.02:g}", "y": f"{y1 - height * 0.02:g}", "font-size": f"{max(height * 0.025, 10):g}", "font-weight": "bold"})
|
|
131
|
+
notice.text = scene.title_block.notice
|
|
132
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
133
|
+
ET.ElementTree(root).write(path, encoding="utf-8", xml_declaration=True)
|
|
134
|
+
ET.parse(path)
|
|
135
|
+
return path
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""OCP tessellation and glTF 2.0 binary scene export."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import struct
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import trimesh
|
|
11
|
+
|
|
12
|
+
from ..determinism import write_json
|
|
13
|
+
from ..exceptions import BuildExportError
|
|
14
|
+
from ..geometry import combined_bounds, tessellate
|
|
15
|
+
from .base import selected_elements
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GlbExporter:
|
|
19
|
+
name = "glb"
|
|
20
|
+
|
|
21
|
+
def __init__(self, linear_deflection: float = 0.5, angular_deflection: float = 0.25) -> None:
|
|
22
|
+
self.linear_deflection = linear_deflection
|
|
23
|
+
self.angular_deflection = angular_deflection
|
|
24
|
+
|
|
25
|
+
def export(self, model, output_dir: Path) -> list[Path]:
|
|
26
|
+
target = output_dir / "glb"
|
|
27
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
scene = trimesh.Scene(base_frame="model")
|
|
29
|
+
expected_ids: list[str] = []
|
|
30
|
+
triangle_counts: dict[str, int] = {}
|
|
31
|
+
for element in selected_elements(model, "glb", physical_only=True):
|
|
32
|
+
vertices, faces = tessellate(element.geometry, self.linear_deflection, self.angular_deflection)
|
|
33
|
+
# CAD Z-up (x,y,z) -> glTF Y-up (x,z,-y), a right-handed rotation.
|
|
34
|
+
converted = np.asarray([[x, z, -y] for x, y, z in vertices], dtype=np.float64)
|
|
35
|
+
material = trimesh.visual.material.PBRMaterial(
|
|
36
|
+
name=element.material.name if element.material else element.id,
|
|
37
|
+
baseColorFactor=[
|
|
38
|
+
int(round(channel * 255)) for channel in (element.color_rgb or (0.7, 0.7, 0.7))
|
|
39
|
+
]
|
|
40
|
+
+ [255],
|
|
41
|
+
metallicFactor=0.0,
|
|
42
|
+
roughnessFactor=0.8,
|
|
43
|
+
)
|
|
44
|
+
mesh = trimesh.Trimesh(vertices=converted, faces=np.asarray(faces), process=False, validate=True)
|
|
45
|
+
mesh.visual = trimesh.visual.TextureVisuals(material=material)
|
|
46
|
+
extras = {
|
|
47
|
+
"stable_id": element.id,
|
|
48
|
+
"category": element.category,
|
|
49
|
+
"source_module": element.source_module,
|
|
50
|
+
"geometry_kind": element.geometry_kind,
|
|
51
|
+
"units": "millimetres",
|
|
52
|
+
}
|
|
53
|
+
scene.add_geometry(mesh, geom_name=element.id, node_name=element.id, metadata=extras)
|
|
54
|
+
expected_ids.append(element.id)
|
|
55
|
+
triangle_counts[element.id] = len(faces)
|
|
56
|
+
path = target / f"{model.artifact_stem}.glb"
|
|
57
|
+
path.write_bytes(trimesh.exchange.gltf.export_glb(scene, include_normals=True))
|
|
58
|
+
document = glb_json(path)
|
|
59
|
+
found_ids = sorted(
|
|
60
|
+
node.get("extras", {}).get("stable_id")
|
|
61
|
+
for node in document.get("nodes", [])
|
|
62
|
+
if node.get("extras", {}).get("stable_id")
|
|
63
|
+
)
|
|
64
|
+
if found_ids != sorted(expected_ids):
|
|
65
|
+
raise BuildExportError(f"GLB node extras missing stable IDs: {found_ids}")
|
|
66
|
+
loaded = trimesh.load(path, force="scene", process=False)
|
|
67
|
+
if not isinstance(loaded, trimesh.Scene) or len(loaded.geometry) != len(expected_ids):
|
|
68
|
+
raise BuildExportError("Independent GLB reload did not preserve element geometry count")
|
|
69
|
+
manifest = write_json(
|
|
70
|
+
target / "manifest.json",
|
|
71
|
+
{
|
|
72
|
+
"schema_id": "urn:python-cad-tools:schema:glb-manifest:1",
|
|
73
|
+
"schema_version": 1,
|
|
74
|
+
"coordinate_transform": "CAD (x,y,z) to glTF (x,z,-y), right-handed",
|
|
75
|
+
"elements": sorted(expected_ids),
|
|
76
|
+
"triangle_counts": triangle_counts,
|
|
77
|
+
"bounds_cad_mm": [
|
|
78
|
+
round(value, 6)
|
|
79
|
+
for value in combined_bounds(
|
|
80
|
+
[element.geometry for element in selected_elements(model, "glb", physical_only=True)]
|
|
81
|
+
)
|
|
82
|
+
],
|
|
83
|
+
"bounds_y_up_mm": np.asarray(loaded.bounds).round(6).tolist(),
|
|
84
|
+
"file_size": path.stat().st_size,
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
return [path, manifest]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def glb_json(path: Path) -> dict:
|
|
91
|
+
data = path.read_bytes()
|
|
92
|
+
if data[:4] != b"glTF":
|
|
93
|
+
raise BuildExportError("Invalid GLB magic")
|
|
94
|
+
_, version, _ = struct.unpack_from("<4sII", data, 0)
|
|
95
|
+
if version != 2:
|
|
96
|
+
raise BuildExportError(f"Expected glTF 2.0, got {version}")
|
|
97
|
+
chunk_length, chunk_type = struct.unpack_from("<II", data, 12)
|
|
98
|
+
if chunk_type != 0x4E4F534A:
|
|
99
|
+
raise BuildExportError("First GLB chunk is not JSON")
|
|
100
|
+
return json.loads(data[20 : 20 + chunk_length].decode("utf-8").rstrip(" \t\r\n\0"))
|