From b9bc51e71abfad0f85d111987924e10a9ccd41e3 Mon Sep 17 00:00:00 2001 From: OpenVMP Date: Sat, 6 Jan 2024 10:04:53 -0800 Subject: [PATCH] Add Assembly YAML aka ASSY (#37) --- README.md | 305 +++++++++++------- examples/assembly_assy/logo.assy | 20 ++ examples/assembly_assy/logo.png | Bin 0 -> 4767 bytes examples/assembly_assy/logo_embedded.assy | 23 ++ examples/assembly_assy/logo_embedded.png | Bin 0 -> 4767 bytes .../partcad.yaml | 18 +- examples/assembly_assy/primitive.assy | 7 + examples/assembly_assy/primitive.png | Bin 0 -> 3925 bytes examples/assembly_logo/logo.png | Bin 3703 -> 0 bytes examples/assembly_logo/logo.py | 26 -- examples/assembly_primitive/assembly.png | Bin 8086 -> 0 bytes examples/assembly_primitive/assembly.py | 22 -- examples/assembly_primitive/partcad.yaml | 9 - examples/part_build123d_primitive/cube.png | Bin 1612 -> 2151 bytes .../part_build123d_primitive/partcad.yaml | 2 +- examples/part_cadquery_logo/bone.png | Bin 1585 -> 1560 bytes examples/part_cadquery_logo/head_half.png | Bin 2657 -> 2217 bytes examples/part_cadquery_logo/partcad.yaml | 5 +- examples/part_cadquery_primitive/cube.png | Bin 1374 -> 2151 bytes examples/part_cadquery_primitive/cylinder.png | Bin 4393 -> 4148 bytes examples/part_cadquery_primitive/partcad.yaml | 2 +- examples/part_step/bolt.png | Bin 10117 -> 5916 bytes examples/part_step/partcad.yaml | 2 +- examples/render_all_examples.sh | 6 + requirements.txt | 6 +- src/partcad/assembly.py | 41 ++- src/partcad/assembly_factory.py | 2 +- src/partcad/assembly_factory_assy.py | 75 +++++ src/partcad/assembly_factory_python.py | 86 ----- src/partcad/cli_add.py | 2 + src/partcad/cli_render.py | 58 +++- src/partcad/part.py | 8 - src/partcad/project.py | 58 ++-- src/partcad/shape.py | 91 ++++-- src/partcad/wrappers/wrapper_build123d.py | 9 +- src/partcad/wrappers/wrapper_cadquery.py | 9 +- src/partcad/wrappers/wrapper_partcad.py | 60 ---- tests/partcad-examples.yaml | 7 +- tests/unit/test_assembly.py | 26 +- tests/unit/test_render.py | 44 +++ 40 files changed, 610 insertions(+), 419 deletions(-) create mode 100644 examples/assembly_assy/logo.assy create mode 100644 examples/assembly_assy/logo.png create mode 100644 examples/assembly_assy/logo_embedded.assy create mode 100644 examples/assembly_assy/logo_embedded.png rename examples/{assembly_logo => assembly_assy}/partcad.yaml (53%) create mode 100644 examples/assembly_assy/primitive.assy create mode 100644 examples/assembly_assy/primitive.png delete mode 100644 examples/assembly_logo/logo.png delete mode 100644 examples/assembly_logo/logo.py delete mode 100644 examples/assembly_primitive/assembly.png delete mode 100644 examples/assembly_primitive/assembly.py delete mode 100644 examples/assembly_primitive/partcad.yaml create mode 100755 examples/render_all_examples.sh create mode 100644 src/partcad/assembly_factory_assy.py delete mode 100644 src/partcad/assembly_factory_python.py delete mode 100644 src/partcad/wrappers/wrapper_partcad.py create mode 100644 tests/unit/test_render.py diff --git a/README.md b/README.md index e2adebe1..8b0a8375 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,15 @@ # PartCAD -[![License](./apache20.svg)](./LICENSE.txt) +[![License](https://github.com/openvmp/partcad/blob/main//apache20.svg?raw=true)](./LICENSE.txt) PartCAD is the first package manager for CAD models, and a Python package to consume these packages in CAD scripts ([`cadquery`][CadQuery] and [`build123d`][build123d]). It brings the same power to CAD scripting -as [pip](https://pypi.org/) to Python, [npm](https://www.npmjs.com/) to JavaScript, [maven](https://maven.apache.org/) to Java etc. +as [pip](https://pypi.org/) to Python, +[npm](https://www.npmjs.com/) to JavaScript, +[maven](https://maven.apache.org/) to Java etc. +Though it aims to achieve the integrity and security properties of +[bazel](https://bazel.build/) which makes PartCAD quite distinct from `pip` and `npm`. [Join our Discord channel!](https://discord.gg/AXbP47zYw5) @@ -17,25 +21,20 @@ The implementation of parts can change over time all of the consumers. - [Installation](#installation) -- [Usage](#usage) - - [Create new package](#create-new-package) - - [List dependencies](#list-dependencies) - - [List available parts and assemblies](#list-available-parts-and-assemblies) - - [Add a dependency](#add-a-dependency) - - [Create a basic assembly](#create-a-basic-assembly) - - [Troubleshooting](#troubleshooting) - - [Render your project](#render-your-project) -- [Modelling](#modelling) +- [Browse models published to PartCAD](#browse-models-published-to-partcad) +- [Consume PartCAD models](#consume-partcad-models) +- [Create PartCAD models](#create-partcad-models) - [Parts](#parts) - [Assemblies](#assemblies) - - [Scenes](#scenes) - [Packages](#packages) + - [Troubleshooting](#troubleshooting) + - [Render your project](#render-your-project) + - [Publishing](#publishing) - [Export](#export) - - [Visualization](#visualization) + - [Images](#images) - [Other modelling formats](#other-modelling-formats) - [Purchasing / Bill of materials](#purchasing--bill-of-materials) - [Security](#security) -- [Public repository](#public-repository) - [Tools for mechanical engineering](#tools-for-mechanical-engineering) - [History](#history) @@ -55,30 +54,33 @@ cd partcad python3 -m pip install -e . ``` -## Usage +PartCAD works best when [conda](https://docs.conda.io/) is installed. +Moreover, on Windows it is recommended to run and build PartCAD from within a `conda` +environment. -### Create new package +## Browse models published to PartCAD -`pc init` to initialize new PartCAD package in the current -directory (create the default `partcad.yaml`). +To browse the public PartCAD repository from the command line: -### List dependencies - -`pc list` to list all (recursively) imported dependencies. - -### List available parts and assemblies +```sh +$ pc init # to initialize new PartCAD package in the current folder +$ pc list # to list all available packages +$ pc list-parts -r # to list all parts in all available packages +$ pc list-assemblies -r # to list all assemblies in all available packages +``` -`pc list-parts -r` and `pc list-assemblies -r` to list all available parts and assemblies. +The web UI to browse the public PartCAD repository is not yet published. -### Add a dependency +## Consume PartCAD models -`pc add ` -to import parts from another package. +As PartCAD has no implicit dependencies built in, the current directory needs to be initialized as a PartCAD package and a dependency on the public PartCAD repository needs to be registered. -### Create a basic assembly +```sh +# Initialize new PartCAD package in the current folder +$ pc init +``` -Here is an example that uses PartCAD to create a sample -assembly. +Alternatively, manually create `partcad.yaml` with the following content: ```yaml # partcad.yaml @@ -86,30 +88,58 @@ import: partcad-index: # Standard public PartCAD repository (needs to be explicitly referenced to be used) type: git url: https://github.com/openvmp/partcad-index.git -assemblies: - assembly_01: # declare an assembly object ``` -```python -# assembly_01.py -import partcad as pc - -assy = pc.Assembly() # create an empty assembly -pc.finalize(assy) # this is the object produced by this PartCAD script -``` - -### Troubleshooting +After this, PartCAD python module can be used to retrieve any model. +The exact way to do this depends on the CAD framework used in your project: -At the moment, the best way to troubleshoot PartCAD is to use VS Code with `OCP CAD Viewer`. -Simply run an assembly script which contains a call to `pc.finalize(...)` (from within the IDE) to have the model displayed. -Alternatively, any part or assembly can be displayed in `OCP CAD Viewer` by using `pc show []` or `pc show -a []`. - -### Render your project + + + + + + +
+# CadQuery +import cadquery as cq +import partcad as pc +part = pc.get_part( + # Part name + "fastener/screw-buttonhead", + # Package name + "standard-metric-cqwarehouse", +).get_cadquery() +... +show_object(part) + +# build123d +import build123d as b3d +import partcad as pc +part = pc.get_part( + # Part name + "fastener/screw-buttonhead", + # Package name + "standard-metric-cqwarehouse", +).get_build123d() +... +show_object(part) +
-Use `pc render` to render PartCAD parts and assemblies -in the current package (the current directory). -## Modelling +## Create PartCAD models This frameworks allows to create large models and scenes, one part at a time, while having parts and assemblies often maintained by third parties. @@ -118,68 +148,103 @@ while having parts and assemblies often maintained by third parties. PartCAD allows to define parts using any of the following methods: -| Method | Example | Result | -| ----------- | ----------------------------------------------------------------------------- | ----------------------------------------------- | -| [STEP] | parts:
  bolt:
    type: step | ![](examples/part_step/bolt.png) | -| [CadQuery] | parts:
  cube:
    type: cadquery | ![](examples/part_cadquery_primitive/cube.png) | -| [build123d] | parts:
  cube:
    type: build123d | ![](examples/part_build123d_primitive/cube.png) | - -Other methods to define parts are coming in soon (e.g. OpenSCAD). + + + + + + + + + + + + + +https://github.com/openvmp/partcad/blob/main/examples/assembly_logo/logo.png?raw=true + + + + + +
Method +Example +Result +
STEP +# partcad.yaml +parts: + bolt: + type: step + +
+Store the model in "bold.step" +
CadQuery +# partcad.yaml +parts: + cube: + type: cadquery +
+
+Place the CadQuery script in "cube.py" +
build123d +# partcad.yaml +parts: + cube: + type: build123d + +
+Place the build123d script in "cube.py" +
+ +Other methods to define parts are coming in soon (e.g. SCAD). ### Assemblies Assemblies are defined as parametrized instructions how to put parts and other assemblies together. - - -```python -import partcad as pc - -if __name__ != "__cqgi__": - from cq_server.ui import ui, show_object - -bolt = pc.get_part("example_part_step", "bolt") -bone = pc.get_part("example_part_cadquery_logo", "bone") -head_half = pc.get_part("example_part_cadquery_logo", "head_half") - -model = pc.Assembly() -model.add(bone, loc=pc.Location((0, 0, 0), (0, 0, 1), 0)) -model.add(bone, loc=pc.Location((0, 0, -2.5), (0, 0, 1), -90)) -model.add(head_half, pc.Location((0, 0, 27.5), (0, 0, 1), 0)) -model.add(head_half, pc.Location((0, 0, 25), (0, 0, 1), -90)) -model.add(bolt, loc=pc.Location((0, 0, 7.5), (0, 0, 1), 0)) -pc.finalize(model, show_object) -``` - - -Assembly parameters can be of two kinds: build time and run time. - -Assembly instances with different build time parameters are different -assemblies, different models. +Currently, PartCAD allows to define parts only using ASSY (Assembly YAML): -Assembly instances with different run time parameters are the same assembly, -just visualized in a different state (e.g. different motion state). - -### Scenes - -Scenes are defined as parametrized instructions how to place assemblies -relative to each other for visualization purposes. - - - -Scenes are intended to be used for visualization, simulation, validation and -testing purposes. Scenes are not intended to be used outside of the package -where they are defined. + + + + + + + +
Example +Result +
+# partcad.yaml +assemblies: + logo: + type: assy + +
+# logo.assy +links: + - part: bone + package: example_part_cadquery_logo + location: [[0,0,0], [0,0,1], 0] + - part: bone + package: example_part_cadquery_logo + location: [[0,0,-2.5], [0,0,1], -90] + - part: head_half + package: example_part_cadquery_logo + name: head_half_1 + location: [[0,0,27.5], [0,0,1], 0] + - part: head_half + package: example_part_cadquery_logo + name: head_half_2 + location: [[0,0,25], [0,0,1], -90] + - part: bolt + package: example_part_step + location: [[0,0,7.5], [0,0,1], 0] +
### Packages -Each PartCAD project is a separate package. +Each project that produces or consumes PartCAD models is a separate PartCAD package. Each package may export parts, assemblies and scenes. Each package may import parts, assemblies and scenes from its dependencies (other PartCAD packages). @@ -205,16 +270,34 @@ import: pythonVersion: <(optional) python version for sandboxing if applicable> ``` +### Troubleshooting + +At the moment, the best way to troubleshoot PartCAD is to use VS Code with `OCP CAD Viewer`. +Any part or assembly can be displayed in `OCP CAD Viewer` by running `pc show []` or `pc show -a []` in a terminal view. + +### Render your project + +Use `pc render` to render PartCAD parts and assemblies +in the current package (the current directory). + +### Publishing + +It is very simple to publish your package to the public PartCAD repository. + +First, you need to publish your own package which defines the models you want to publish. +Then create a pull request in [the public PartCAD repo](https://github.com/openvmp/partcad) to add a reference to your package. ## Export -### Visualization +### Images Individual parts, assemblies and scenes can be rendered and exported into the following formats: - PNG - [STL](https://en.wikipedia.org/wiki/STL_(file_format)) (not yet) +- [STEP] (not yet) +- ... ### Other modelling formats @@ -222,6 +305,7 @@ Additionally, assemblies and scenes can be exported into the following formats: - [SDF](http://sdformat.org/) (not yet / in progress) - [FreeCAD](https://www.freecad.org/) project (not yet / in progress) +- ... ### Purchasing / Bill of materials @@ -241,13 +325,6 @@ in the future PartCAD aims to achieve security isolation of the sandboxed environments. That will fundamentally change the security implications of using scripted models shared online. -## Public repository - -PartCAD allows anyone to create their own private repositories of parts. -However it also comes with a -[public repository](https://github.com/openvmp/partcad-index) -that is used by default for all new packages. - ## Tools for mechanical engineering Here is an overview of the open source tools to maintain @@ -266,7 +343,7 @@ subgraph repo["Your project's GIT repository"] custom_part_os["Another reusable part\nmaintained as a script\nunder a version control system"] end - model["Your project's model\ndefined as Python code\nfor version control\nand collaboration"] + model["Your project's model defined\nas ASSY or Python code\nfor version control\nand collaboration"] subgraph scenes["Scenes"] test1["Capability 1\ntest scene"] @@ -281,7 +358,7 @@ end subgraph external_tools["External tools"] freecad["FreeCAD"] - cadquery["CadQuery"] + cadquery["CadQuery / build123d"] openscad["OpenSCAD"] gazebo["Gazebo"] @@ -315,10 +392,10 @@ internally in [OpenVMP](https://github.com/openvmp/openvmp-models). It is now being maintained separately as a generic tool. The motivation behind this framework is to build a packaging and dependency -tracking layer on top of [CadQuery] and traditional CAD tools to enable -version control and other features required for effective collaboration. +tracking layer on top of both [CadQuery]/[build123d] and traditional CAD tools to +enable version control and other features required for effective collaboration. -This framework currently uses [CadQuery] and, thus, [OpenCASCADE] under the hood. +This framework currently uses [build123d] and, thus, [OpenCASCADE] under the hood. However this may change in the future, if the python C bindings for [OpenCASCADE] remain a blocker for unlocking multithreaded performance. diff --git a/examples/assembly_assy/logo.assy b/examples/assembly_assy/logo.assy new file mode 100644 index 00000000..2062b498 --- /dev/null +++ b/examples/assembly_assy/logo.assy @@ -0,0 +1,20 @@ +links: + - part: bone + package: example_part_cadquery_logo + name: bone1 + location: [[0,0,0], [0,0,1], 0] + - part: bone + package: example_part_cadquery_logo + name: bone2 + location: [[0,0,-2.5], [0,0,1], -90] + - part: head_half + package: example_part_cadquery_logo + name: head_half_1 + location: [[0,0,27.5], [0,0,1], 0] + - part: head_half + package: example_part_cadquery_logo + name: head_half_2 + location: [[0,0,25], [0,0,1], -90] + - part: bolt + package: example_part_step + location: [[0,0,7.5], [0,0,1], 0] \ No newline at end of file diff --git a/examples/assembly_assy/logo.png b/examples/assembly_assy/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0ba13c84e62eae1c09a240b4a3445180fa9b0173 GIT binary patch literal 4767 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 MsfW_7(YBBI4+!d!)Bpeg literal 0 HcmV?d00001 diff --git a/examples/assembly_logo/logo.png b/examples/assembly_logo/logo.png deleted file mode 100644 index ee14ae179d4873b82df8dad42348632e9d4e7b1f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3703 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 literal 1612 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 diff --git a/examples/part_cadquery_logo/head_half.png b/examples/part_cadquery_logo/head_half.png index e630a017ae1902f629e3603f08be6adcdc5266c6..256857b14fdbddbb990ceb05378aa52cbe2a339e 100644 GIT binary patch literal 2217 zcmbtW`#TeS8^=SIB(LU>6xBkt2R)YKa>{auv0>&cIY(m-b0#!N4i67=K84jZbI5Tr zhZnK3*Tbx#6tPFc2swn__5KU*58v;7-{0%{+}C~G_vijx-*j8J*-0^ZF#!RAlNRQt zNWQh>pCo_~|K5%1pArxNgjkpw+eekImfa64wgL|KJ_Zmw#MI#*LR z=KaQ01eSv6PHJC_4lS1=bzB6`iyyd0keUw$*ezV$HQiTt<$wsS5^L&*x&ZkLrU?61 zXX{#|cbbxrwQV$oRl?u^s?yR#i|alc`Z$%p^Hmsjb9c{`e+&@*%SJh`&(r9C2;xK= z4*~RPLIX1f)jfKm6taM8`Il0GFefr9-E@9`e|p5s6j2k<<*GKk5QZ+!wsA%(I?zXN|eI&rd90+q`Rg{;*HRp-g zR9oVN6NSYND(G;~7rrpL8u>31ZEfv@Y}i@6Ad1Y5QQ8b9Fawzn+E1saQudcC3=@xb zH!hXD+tr5~OXDMIcqno8EeoHbnJNuNZ<8Is%8FlS^ z_KaR9Gw_C-c9Wa%i$zapk7%|R~Po^MIsQZuygohCxQ0!+c&QdOl7I#Sqta_ zY2m7w)!eRoNbeS>tOZGvWF;%*(`gm^oT3uDX^p3So|WAFHBg$!P_{?`;7Rq<;RS4G zTnNUOqR^|Oi}nu-2RTD9OU;IUUKtuc)iKGPBi!-QTMWJcEg$b>T19nkS}b3mw#28;3rG2vbO?d#3iT@=!@9S&mjQK zs|0$H`ojbm0zXit$c8lr7T59C*1p3~-cD1H-}6Rx^VwH{VvkIW5kO@7A;68(gMpDLZ@b6F05*J+AGbEnuPwq!sV)DsE)M+*0tIlX8y zioX+_Xzm^28{azQm8=DCCzM00ttwh2`q_BpucRa1^H8dP>(_XXB8j>oIAQokSt-cE z1ujwkvv1{MoH72+xmRB;Ym-k+R_LXi+{fX@fiPxZYdjmaJJou>Ng0*c!@KDsoJZBh zjdwL@msn4Xj6*&IX&K`ixO;K#4ooz7DoAVROfq*8okMdLxFib;)hf9Og%;*PWimd@ z&c0-E2?PR%<2S=_KJ27!*{#zYuWw^?JENdIB?FNicS23pM<1rZAK-Yae}*9>&}9k- zsl6j=gXcK4rOi5>EEa3rUpQ9K@R*+dq7hhzN~7)I03MWtG1Jijvl?jJ|8LT0wAj^1 zDt~;h-Dy2NOPhNwdX?M^Y>T`<8Ks%=x_m}3ICr#4Z4le(E$6!PtbH%wJ7wb^N9V```_=C@;Q|; zC}dZoVg#}NvAb(ti-eOlKX*xcmX?C)c_W~lb03WrP(SleD-hs$p&K`B2w-&+oqQ_;-f2yKYh_wvexg z@QaUgI9sq^VPmhAP%F;#vH| z<87E#q{2rd&r!i$nY(8X{sLJPEOI!Ucp2_>#+gQ}XGLC1@THIt!hBB4qyEgSn1sYb-yyHhy_$QLu)o>?w(jU^y1_KRL=!v}b%Bz?&X6gJdh=2tQ KZrWhtnfzZaqhN^u literal 2657 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 literal 1374 zcmd6n|5MU;9LHZD#)L{t>q2%K5$0shuui3T?{DGUs63?QXY!V6XSx>t6SKyncA!J)ieF zyFZGCTTECC066+yN;ClC7(s${arAMG)(Kp=3>pO%oUgK_Rt50>ayn(#!OTn6A#&2E ziFn!Xb6Kj4>4cM;HM=oOpM=YnMc8Xu+xmSy*2~1w>^!lou!g1U3&?e?iVI*=48Zc)dM}e?wnAsfu-D{b?9-QNJ{wWe$5~q%g~|wuqxGaDNr}@f#TUy~ z9LgqHRJxS0MZS&wb$`Swqrb^W6El;mH|#vx^je|(C`F;2+@=Eb%PsKB9Z6G8!o#O> z@+Vm%uX6fku@d2w@Kpu0x!2Mt&`K&}V)2B82={a?VT<3D364|OWLC&~^?)P}c> zo*J`vwO*6P3W%Ei`1&2kM>1y0t3RaA3Cec^Dt+d(O;7rAfOp%w)|+NcofAhBPUs)| zEPW%rspH3ET3UvS#*#X&h41O?(EIfi8`oN{|4?vRFq|L|r;b;JObwl!DQ61(lNn0W zK4tgaOBJq1)cxsng0^E&Fp?H!DI4aA=mQ^I_tCx0ZNgElUCS>vw{Kho!+q6wo~J}6 zci3JTZ417?25>%3Zx5!)cO@fV%sRbKC}y}GB1l3BPEgb9C9T15B57h^} z%LU`R0{o^;&`NcJQ>KE}8kBpcGcX%9_wf&WfSTimge3=hOY2bvGl>Eh^0K)5y?{y% zeU}ZPL!JnS3z~D>(dh;gt6M}R6H$`(S-$JKu@WOwE)GP)`CidbizP27fL=VMcvkkT iN5}u~LHg%9m3EQ`JhpqSxf}DFQ={*RqBMkazWxgi9y>+= diff --git a/examples/part_cadquery_primitive/cylinder.png b/examples/part_cadquery_primitive/cylinder.png index b73cb75e617a83b5061ecb8670c8245ffb5a98b5..3c3556bbb6c8b495ec098f0cd36ab38b981ee4be 100644 GIT binary patch literal 4148 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 diff --git a/examples/part_cadquery_primitive/partcad.yaml b/examples/part_cadquery_primitive/partcad.yaml index fa702e5f..3b2648f6 100644 --- a/examples/part_cadquery_primitive/partcad.yaml +++ b/examples/part_cadquery_primitive/partcad.yaml @@ -10,6 +10,6 @@ parts: render: png: prefix: ./ - width: 256 + width: 128 height: 128 markdown: README.md diff --git a/examples/part_step/bolt.png b/examples/part_step/bolt.png index 1720da5b9d3c3fb2a84b2f1fd97326fd8671fe8c..1aaa0c930f1cefd48248e3f279c03b5f7ddfddc1 100644 GIT binary patch literal 5916 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�FPev2s>S>IFYd4Or(J<# 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.3.1 numpy>=1.25.2 scipy>=1.11.1 #tox==4.9.0 pyyaml==6.0.1 -cairosvg==2.7.0 +pycairo +renderlab +rlPyCairo +svglib ocp_vscode==2.0.13 GitPython==3.1.40 progress==1.6 diff --git a/src/partcad/assembly.py b/src/partcad/assembly.py index 875e3271..2ef19df2 100644 --- a/src/partcad/assembly.py +++ b/src/partcad/assembly.py @@ -15,6 +15,13 @@ from . import shape +class AssemblyChild: + def __init__(self, item, name=None, location=None): + self.item = item + self.name = name + self.location = location + + class Assembly(shape.Shape): def __init__(self, name=None, config={}): super().__init__(name) @@ -26,6 +33,10 @@ def __init__(self, name=None, config={}): ) else: self.name = name + if "location" in config: + self.location = config["location"] + else: + self.location = None self.shape = None # self.children contains all child parts and assemblies @@ -36,31 +47,39 @@ def __init__(self, name=None, config={}): def add( self, - child: shape.Shape, # pc.Part or pc.Assembly + child_item: shape.Shape, # pc.Part or pc.Assembly name=None, loc=b3d.Location((0.0, 0.0, 0.0), (0.0, 0.0, 1.0), 0.0), ): - child_item = copy.copy(child.get_build123d()).locate(loc) # Shallow copy - # child_item.label = name # TODO(clairbee): fix this - self.children.append(child_item) + self.children.append(AssemblyChild(child_item, name, loc)) # Keep part reference counter for bill-of-materials purposes - child.ref_inc() + child_item.ref_inc() # Destroy the previous object if any self.shape = None def ref_inc(self): for child in self.children: - child.ref_inc() + child.item.ref_inc() def get_shape(self): if self.shape is None: - self.shape = b3d.Compound(label=self.name, children=self.children) - return self.shape - - def get_build123d(self): - return copy.copy(self.get_shape()) + child_shapes = [] + for child in self.children: + item = copy.copy(child.item.get_build123d()) + if not child.name is None: + item.label = child.name + if not item.location is None: + item.locate(child.location) + child_shapes.append(item) + shape = b3d.Compound(children=child_shapes) + if not self.name is None: + shape.label = self.name + if not self.location is None: + shape.locate(self.location) + self.shape = shape.wrapped + return copy.copy(self.shape) def get_wrapped(self): return self.get_shape() diff --git a/src/partcad/assembly_factory.py b/src/partcad/assembly_factory.py index a1deb5aa..baa0a30e 100644 --- a/src/partcad/assembly_factory.py +++ b/src/partcad/assembly_factory.py @@ -25,7 +25,7 @@ def __init__(self, ctx, project, assembly_config, extension=""): raise Exception("ERROR: The project path must be a directory") self.path = os.path.join(project.path, self.path) if not os.path.exists(self.path): - raise Exception("ERROR: The part path must exist") + raise Exception("ERROR: The assembly path must exist") # Pass the autodetected path to the 'Assembly' class assembly_config["path"] = self.path diff --git a/src/partcad/assembly_factory_assy.py b/src/partcad/assembly_factory_assy.py new file mode 100644 index 00000000..b32b83d2 --- /dev/null +++ b/src/partcad/assembly_factory_assy.py @@ -0,0 +1,75 @@ +# +# OpenVMP, 2023 +# +# Author: Roman Kuzmenko +# Created: 2024-01-06 +# +# Licensed under Apache License, Version 2.0. +# + +import build123d as b3d +import copy +import logging +import os +import yaml + +from .assembly import Assembly +from . import assembly_factory as af + + +class AssemblyFactoryAssy(af.AssemblyFactory): + def __init__(self, ctx, project, assembly_config): + super().__init__(ctx, project, assembly_config, extension=".assy") + # Complement the config object here if necessary + self._create(assembly_config) + + self.assy = {} + if os.path.exists(self.path): + try: + self.assy = yaml.safe_load(open(self.path)) + except Exception as e: + logging.error("ERROR: Failed to parse the assembly file %s" % self.path) + if self.assy is None: + self.assy = {} + else: + logging.error("ERROR: Assembly file not found: %s" % self.path) + + if "links" in self.assy and not self.assy["links"] is None: + self.handle_node_list(self.assembly, self.assy["links"]) + + self._save() + + def handle_node_list(self, assembly, node_list): + for link in node_list: + self.handle_node(assembly, link) + + def handle_node(self, assembly, node): + # "name" is an optional parameter for both parts and assemblies + if "name" in node: + name = node["name"] + else: + name = None + + # "location" is an optional parameter for both parts and assemblies + if "location" in node: + l = node["location"] + location = b3d.Location( + (l[0][0], l[0][1], l[0][2]), (l[1][0], l[1][1], l[1][2]), l[2] + ) + else: + location = b3d.Location((0, 0, 0), (0, 0, 1), 0) + + # Check if this node is for an assembly + if "links" in node: + item = Assembly(name, node["links"]) + self.handle_node_list(item, node["links"]) + else: + # This is a node for a part + if "package" in node: + package_name = node["package"] + else: + package_name = "this" + part_name = node["part"] + item = self.ctx.get_part(part_name, package_name) + + assembly.add(item, name, location) diff --git a/src/partcad/assembly_factory_python.py b/src/partcad/assembly_factory_python.py deleted file mode 100644 index c1ee8508..00000000 --- a/src/partcad/assembly_factory_python.py +++ /dev/null @@ -1,86 +0,0 @@ -# -# OpenVMP, 2023 -# -# Author: Roman Kuzmenko -# Created: 2023-08-19 -# -# Licensed under Apache License, Version 2.0. -# - -# import base64 -import os - -# import pickle -import sys - -from . import assembly_factory as af - -# from . import wrapper - -from cadquery import cqgi - -sys.path.append(os.path.join(os.path.dirname(__file__), "wrappers")) -from cq_serialize import ( - register as register_cq_helper, -) # import this one for `pickle` to use - - -class AssemblyFactoryPython(af.AssemblyFactory): - def __init__(self, ctx, project, assembly_config): - super().__init__(ctx, project, assembly_config, extension=".py") - # Complement the config object here if necessary - self._create(assembly_config) - - cadquery_script = open(self.path, "r").read() - - # TODO(clairbee): this is a workaround to lack of support for 'atexit()' - # in CQGI - if "import partcad as pc" in cadquery_script: - cadquery_script += "\npc.finalize_real()\n" - # print(cadquery_script) - - script = cqgi.parse(cadquery_script) - result = script.build(build_parameters={}) - if result.success: - shape = result.first_result.shape - else: - shape = None - - if hasattr(shape, "val"): - shape = shape.val() - if hasattr(shape, "wrapped"): - shape = shape.wrapped - self.assembly.shape = shape - - # TODO(clairbee): Continue using CQGI (above) instead of a wrapper - # process(below) until there an IPC mechanism to share - # the PartCAD context. - # - # self.runtime = self.ctx.get_python_runtime( - # self.project.python_version, - # # python_runtime="none", - # ) - # self.runtime.prepare_for_package(project) - - # wrapper_path = wrapper.get("partcad.py") - - # request = {"build_parameters": {}} - # picklestring = pickle.dumps(request) - # request_serialized = base64.b64encode(picklestring).decode() - - # self.runtime.ensure("partcad") - # response_serialized, errors = self.runtime.run( - # [wrapper_path, self.path], request_serialized - # ) - # sys.stderr.write(errors) - - # response = base64.b64decode(response_serialized) - # result = pickle.loads(response) - - # if result["success"]: - # shape = result["shape"] - # self.part.shape = shape - # else: - # print(result["exception"]) - - self._save() diff --git a/src/partcad/cli_add.py b/src/partcad/cli_add.py index 728978a7..3a97cd56 100644 --- a/src/partcad/cli_add.py +++ b/src/partcad/cli_add.py @@ -51,6 +51,8 @@ def cli_add(args): yaml.preserve_quotes = True with open("partcad.yaml") as fp: config = yaml.load(fp) + fp.close() + for elem in config: if elem == "import": imports = config["import"] diff --git a/src/partcad/cli_render.py b/src/partcad/cli_render.py index 45de4c20..95ab8136 100644 --- a/src/partcad/cli_render.py +++ b/src/partcad/cli_render.py @@ -1,5 +1,5 @@ # -# OpenVMP, 2023 +# OpenVMP, 2023-2024 # # Author: Roman Kuzmenko # Created: 2023-12-23 @@ -7,13 +7,45 @@ # Licensed under Apache License, Version 2.0. # +import argparse + import partcad as pc -def cli_help_render(subparsers): +def cli_help_render(subparsers: argparse.ArgumentParser): parser_render = subparsers.add_parser( "render", - help="Render parts, assemblies and scenes in this package", + help="Render the selected or all parts, assemblies and scenes in this package", + ) + group_type = parser_render.add_mutually_exclusive_group(required=False) + group_type.add_argument( + "-a", + help="The object is an assembly", + dest="assembly", + action="store_true", + ) + group_type.add_argument( + "-s", + help="The object is a scene", + dest="scene", + action="store_true", + ) + + parser_render.add_argument( + "object", + help="Part (default), assembly or scene to render", + type=str, + nargs="?", + ) + # TODO(clairbee): make the package argument depending on the object argument + # [ []] + # instead of + # [] [] + parser_render.add_argument( + "package", + help="Package to retrieve the object from", + type=str, + nargs="?", ) @@ -23,4 +55,22 @@ def cli_render(args): else: ctx = pc.init() - ctx.render() + if args.package is None: + package = "this" + else: + package = args.package + + if args.object is None: + # Render all parts and assemblies configured to be auto-rendered in this project + ctx.render() + else: + # Render the requested part or assembly + parts = [] + assemblies = [] + if args.assembly: + assemblies.append(args.object) + else: + parts.append(args.object) + + prj = ctx.get_project(package) + prj.render(parts=parts, assemblies=assemblies) diff --git a/src/partcad/part.py b/src/partcad/part.py index 23ed4cb2..505f3f50 100644 --- a/src/partcad/part.py +++ b/src/partcad/part.py @@ -57,14 +57,6 @@ def clone(self): cloned.count = self.count return cloned - def get_build123d(self): - b3d_solid = b3d.Solid.make_box(1, 1, 1) - b3d_solid.wrapped = self.get_wrapped() - return b3d_solid - - def get_wrapped(self): - return self.shape - def _render_txt_real(self, file): file.write(self.name + ": " + self.count + "\n") diff --git a/src/partcad/project.py b/src/partcad/project.py index 874152b0..7703efed 100644 --- a/src/partcad/project.py +++ b/src/partcad/project.py @@ -13,7 +13,7 @@ from . import part_factory_step as pfs from . import part_factory_cadquery as pfc from . import part_factory_build123d as pfb -from . import assembly_factory_python as afp +from . import assembly_factory_assy as afa class Project(project_config.Configuration): @@ -87,8 +87,7 @@ def get_part(self, part_name): pfs.PartFactoryStep(self.ctx, self, part_config) else: logging.error( - "Invalid repository type encountered: %s: %s" - % (part_name, part_config) + "Invalid part type encountered: %s: %s" % (part_name, part_config) ) return None @@ -122,7 +121,17 @@ def get_assembly(self, assembly_name): # TODO(clairbee): reconsider passing the name as a parameter assembly_config["name"] = assembly_name - afp.AssemblyFactoryPython(self.ctx, self, assembly_config) + if assembly_config["type"] == "assy": + logging.info( + "Initializing AssemblyYAML assembly: %s..." % assembly_name + ) + afa.AssemblyFactoryAssy(self.ctx, self, assembly_config) + else: + logging.error( + "Invalid assembly type encountered: %s: %s" + % (assembly_name, assembly_config) + ) + return None # Since factories do not return status codes, we need to verify # whether they have produced the expected product or not @@ -135,44 +144,31 @@ def get_assembly(self, assembly_name): return self.assemblies[assembly_name] - def render(self): + def render(self, parts=None, assemblies=None): logging.info("Rendering the project: %s" % self.path) if not "render" in self.config_obj: return render = self.config_obj["render"] - parts = {} - if "parts" in self.config_obj: - parts = self.config_obj["parts"].keys() - assemblies = {} - if "assemblies" in self.config_obj: - assemblies = self.config_obj["assemblies"].keys() - + # Enumerating all parts and assemblies + if parts is None: + parts = [] + if "parts" in self.config_obj: + parts = self.config_obj["parts"].keys() + if assemblies is None: + assemblies = [] + if "assemblies" in self.config_obj: + assemblies = self.config_obj["assemblies"].keys() + + # See whether PNG is configured to be auto-rendered or not if "png" in render: logging.info("Rendering PNG...") - if isinstance(render["png"], str): - render_path = render["png"] - render_width = None - render_height = None - else: - png = render["png"] - render_path = png["prefix"] - render_width = png["width"] - render_height = png["height"] for part_name in parts: part = self.get_part(part_name) if not part is None: - part.render_png( - render_path + part_name + ".png", - width=render_width, - height=render_height, - ) + part.render_png(project=self) for assembly_name in assemblies: assembly = self.get_assembly(assembly_name) if not assembly is None: - assembly.render_png( - render_path + assembly_name + ".png", - width=render_width, - height=render_height, - ) + assembly.render_png(project=self) diff --git a/src/partcad/shape.py b/src/partcad/shape.py index f1ab00c4..fb0fbd16 100644 --- a/src/partcad/shape.py +++ b/src/partcad/shape.py @@ -9,8 +9,10 @@ import cadquery as cq import build123d as b3d -import cairosvg +from svglib.svglib import svg2rlg +from reportlab.graphics import renderPM import logging +import os import tempfile from .render import * @@ -26,13 +28,18 @@ def __init__(self, name): self.svg_path = None self.svg_url = None + def get_wrapped(self): + return self.shape + def get_cadquery(self) -> cq.Shape: cq_solid = cq.Solid.makeBox(1, 1, 1) cq_solid.wrapped = self.get_wrapped() return cq_solid - def get_build123d(self) -> b3d.Shape: - raise Exception("Part or Assembly must redefine get_build123d") + def get_build123d(self) -> b3d.Solid: + b3d_solid = b3d.Solid.make_box(1, 1, 1) + b3d_solid.wrapped = self.get_wrapped() + return b3d_solid def show(self, show_object=None): shape = self.get_wrapped() @@ -108,12 +115,22 @@ def render_svg(self, filepath=None, opt=DEFAULT_RENDER_SVG_OPTS): if filepath is None: filepath = tempfile.mktemp(".svg") - cq_obj = self.get_cadquery() - cq.exporters.export( - cq_obj, - filepath, - opt=opt, + viewport_origin = (100, -100, 100) + b3d_obj = self.get_build123d() + visible, hidden = b3d_obj.project_to_viewport( + viewport_origin, ) + max_dimension = max( + *b3d.Compound(children=visible + hidden).bounding_box().size + ) + exporter = b3d.ExportSVG(scale=100 / max_dimension) + exporter.add_layer("Visible", fill_color=(224, 224, 48)) + # exporter.add_layer( + # "Hidden", line_color=(99, 99, 99), line_type=b3d.LineType.ISO_DOT + # ) + exporter.add_shape(visible, layer="Visible", reverse_wires=False) + # exporter.add_shape(hidden, layer="Hidden") + exporter.write(filepath) self.svg_path = filepath @@ -131,21 +148,59 @@ def _get_svg_url(self): def render_png( self, - filepath, - width=DEFAULT_RENDER_WIDTH, - height=DEFAULT_RENDER_HEIGHT, + project=None, + filepath=None, + width=None, + height=None, ): + if ( + not project is None + and "render" in project.config_obj + and not project.config_obj["render"] is None + ): + render_opts = project.config_obj["render"] + else: + render_opts = {} + + if "png" in render_opts and not render_opts["png"] is None: + if isinstance(render_opts["png"], str): + png_opts = {"prefix": render_opts["png"]} + else: + png_opts = render_opts["png"] + else: + png_opts = {} + + # Using the project's config defaults if any if filepath is None: - filepath = self.path + "/part.png" + if "prefix" in png_opts and not png_opts["prefix"] is None: + filepath = os.path.join(png_opts["prefix"], self.name + ".png") + else: + filepath = self.name + ".png" + + if width is None: + if "width" in png_opts and not png_opts["width"] is None: + width = png_opts["width"] + else: + width = DEFAULT_RENDER_WIDTH + if height is None: + if "height" in png_opts and not png_opts["height"] is None: + height = png_opts["height"] + else: + height = DEFAULT_RENDER_HEIGHT + + # Render the vector image logging.info("Rendering: %s" % filepath) svg_path = self._get_svg_path() - cairosvg.svg2png( - url=svg_path, - write_to=filepath, - output_width=width, - output_height=height, - ) + # Render the raster image + drawing = svg2rlg(svg_path) + scale_width = float(width) / float(drawing.width) + scale_height = float(height) / float(drawing.height) + scale = min(scale_width, scale_height) + drawing.scale(scale, scale) + drawing.width *= scale + drawing.height *= scale + renderPM.drawToFile(drawing, filepath, fmt="PNG") def render_txt(self, filepath=None): if filepath is None: diff --git a/src/partcad/wrappers/wrapper_build123d.py b/src/partcad/wrappers/wrapper_build123d.py index 99b1c8ca..c7ddb14d 100644 --- a/src/partcad/wrappers/wrapper_build123d.py +++ b/src/partcad/wrappers/wrapper_build123d.py @@ -24,8 +24,13 @@ def process(path, request): if "build_parameters" in request: build_parameters = request["build_parameters"] - cadquery_script = open(path, "r").read() - script_object = cqgi.parse(cadquery_script) + script = open(path, "r").read() + if "import partcad" in script: + script = ( + "import logging\nlogging.basicConfig(level=60)\n" # Disable PartCAD logging + + script + ) + script_object = cqgi.parse(script) result = script_object.build(build_parameters=build_parameters) if not result.success: diff --git a/src/partcad/wrappers/wrapper_cadquery.py b/src/partcad/wrappers/wrapper_cadquery.py index a7c67717..d9b2cff0 100644 --- a/src/partcad/wrappers/wrapper_cadquery.py +++ b/src/partcad/wrappers/wrapper_cadquery.py @@ -24,8 +24,13 @@ def process(path, request): if "build_parameters" in request: build_parameters = request["build_parameters"] - cadquery_script = open(path, "r").read() - script_object = cqgi.parse(cadquery_script) + script = open(path, "r").read() + if "import partcad" in script: + script = ( + "import logging\nlogging.basicConfig(level=60)\n" # Disable PartCAD logging + + script + ) + script_object = cqgi.parse(script) result = script_object.build(build_parameters=build_parameters) if not result.success: diff --git a/src/partcad/wrappers/wrapper_partcad.py b/src/partcad/wrappers/wrapper_partcad.py deleted file mode 100644 index 7a7e7c2b..00000000 --- a/src/partcad/wrappers/wrapper_partcad.py +++ /dev/null @@ -1,60 +0,0 @@ -# OpenVMP, 2023-2024 -# -# Author: Roman Kuzmenko -# Created: 2024-01-01 -# -# Licensed under Apache License, Version 2.0. -# - -# This script is executed within the python sandbox environment (python runtime) -# to invoke PartCAD assembly scripts. - -import os -import sys - -from cadquery import cqgi - -sys.path.append(os.path.dirname(__file__)) -import wrapper_common - - -def process(path, request): - build_parameters = {} - if "build_parameters" in request: - build_parameters = request["build_parameters"] - - cadquery_script = ( - "import logging\nlogging.basicConfig(level=60)\n" # Disable PartCAD logging - ) - # TODO(clairbee): add smth to the script to suppress ocp_vscode calls - cadquery_script += open(path, "r").read() - script_object = cqgi.parse(cadquery_script) - result = script_object.build(build_parameters=build_parameters) - - if not result.success: - sys.stderr.write("Exception: ") - sys.stderr.write(str(result.exception)) - - shape = None if not result.first_result else result.first_result.shape - if hasattr(shape, "val"): - shape = shape.val() - # TODO(clairbee): do not coumpund assemblies for better visualization - if hasattr(shape, "toCompound"): - shape = shape.toCompound() - if hasattr(shape, "wrapped"): - shape = shape.wrapped - - return { - "success": result.success, - "exception": result.exception, - "shape": shape, - } - - -path, request = wrapper_common.handle_input() - -# Call CadQuery -model = process(path, request) - -output = wrapper_common.handle_output(model) -print(output) diff --git a/tests/partcad-examples.yaml b/tests/partcad-examples.yaml index d0e8dc1b..1bdadaa6 100644 --- a/tests/partcad-examples.yaml +++ b/tests/partcad-examples.yaml @@ -22,9 +22,6 @@ import: example_part_build123d_primitive: type: local path: ../examples/part_build123d_primitive - example_assembly_primitive: + example_assembly_assy: type: local - path: ../examples/assembly_primitive - example_assembly_logo: - type: local - path: ../examples/assembly_logo + path: ../examples/assembly_assy diff --git a/tests/unit/test_assembly.py b/tests/unit/test_assembly.py index 5fd066fd..f9cb782e 100644 --- a/tests/unit/test_assembly.py +++ b/tests/unit/test_assembly.py @@ -11,7 +11,7 @@ import partcad as pc -def test_assembly_primitive1(): +def test_assembly_primitive(): ctx = pc.init("tests/partcad-examples.yaml") part1 = ctx.get_part("cube", "example_part_cadquery_primitive") assert part1 is not None @@ -24,14 +24,28 @@ def test_assembly_primitive1(): ctx.finalize(model, None) -def test_assembly_example_primitive(): +def test_assembly_example_assy_primitive(): ctx = pc.init("tests/partcad-examples.yaml") - assembly = ctx.get_assembly("assembly", "example_assembly_primitive") - assert assembly is not None + primitive = ctx.get_assembly("primitive", "example_assembly_assy") + assert primitive is not None + assert primitive.get_cadquery() is not None + assert primitive.get_build123d() is not None + assert primitive.get_shape() is not None -def test_assembly_example_logo(): +def test_assembly_example_assy_logo(): ctx = pc.init("tests/partcad-examples.yaml") - logo = ctx.get_assembly("logo", "example_assembly_logo") + logo = ctx.get_assembly("logo", "example_assembly_assy") assert logo is not None + assert logo.get_cadquery() is not None + assert logo.get_build123d() is not None + assert logo.get_shape() is not None + + +def test_assembly_example_assy_logo_embedded(): + ctx = pc.init("tests/partcad-examples.yaml") + logo = ctx.get_assembly("logo_embedded", "example_assembly_assy") + assert logo is not None + assert logo.get_cadquery() is not None + assert logo.get_build123d() is not None assert logo.get_shape() is not None diff --git a/tests/unit/test_render.py b/tests/unit/test_render.py new file mode 100644 index 00000000..cc93472c --- /dev/null +++ b/tests/unit/test_render.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# +# OpenVMP, 2024 +# +# Author: Roman Kuzmenko +# Created: 2024-01-06 +# +# Licensed under Apache License, Version 2.0. +# + +import partcad as pc + + +def test_render_svg_part_1(): + """Render a primitive shape to SVG""" + ctx = pc.init("tests/partcad-examples.yaml") + cube = ctx.get_part("cube", "example_part_cadquery_primitive") + assert cube is not None + try: + cube.render_svg() + except Exception as e: + assert False, "Valid render request caused an exception: %s" % e + + +def test_render_svg_assy_1(): + """Render a primitive shape to SVG""" + ctx = pc.init("tests/partcad-examples.yaml") + assy = ctx.get_assembly("logo", "example_assembly_assy") + assert assy is not None + try: + assy.render_svg() + except Exception as e: + assert False, "Valid render request caused an exception: %s" % e + + +def test_render_svg_assy_2(): + """Render a primitive shape to SVG""" + ctx = pc.init("tests/partcad-examples.yaml") + assy = ctx.get_assembly("logo_embedded", "example_assembly_assy") + assert assy is not None + try: + assy.render_svg() + except Exception as e: + assert False, "Valid render request caused an exception: %s" % e