Skip to content

Commit

Permalink
Added support for sketches defined using CadQuery (#192)
Browse files Browse the repository at this point in the history
  • Loading branch information
openvmp authored Oct 10, 2024
1 parent ba6e118 commit 9037e90
Show file tree
Hide file tree
Showing 14 changed files with 310 additions and 10 deletions.
7 changes: 6 additions & 1 deletion docs/source/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ Sketches are declared in ``partcad.yaml`` using the following syntax:
sketches:
<sketch-name>:
type: <basic|dxf|svg>
type: <basic|dxf|svg|cadquery|build123d>
desc: <(optional) textual description>
path: <(optional) the source file path, "{sketch name}.{ext}" otherwise>
# ... type-specific options ...
Expand Down Expand Up @@ -178,6 +178,11 @@ Such sketches are declared using the following syntax:
ignore-visibility: <(optional) boolean>
flip-y: <(optional) boolean>
CAD Scripts
-----------

See the "CAD Scripts" section in the "Parts" chapter below.

==========
Interfaces
==========
Expand Down
21 changes: 21 additions & 0 deletions examples/produce_sketch_cadquery/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# /pub/examples/partcad/produce_sketch_cadquery

This example demonstrates how to define a sketch using a CadQuery script.

## Usage
```shell
pc inspect -s sketch
```


## Sketches

### sketch
<table><tr>
<td valign=top><a href="sketch.py"><img src="././sketch.svg" style="width: auto; height: auto; max-width: 200px; max-height: 200px;"></a></td>
<td valign=top>A sample sketch</td>
</tr></table>

<br/><br/>

*Generated by [PartCAD](https://partcad.org/)*
16 changes: 16 additions & 0 deletions examples/produce_sketch_cadquery/partcad.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
desc: This example demonstrates how to define a sketch using a CadQuery script.

docs:
usage: |
```shell
pc inspect -s sketch
```
sketches:
sketch:
type: cadquery
desc: A sample sketch

render:
readme:
svg:
11 changes: 11 additions & 0 deletions examples/produce_sketch_cadquery/sketch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import cadquery as cq

sk1 = (
cq.Sketch()
.rect(3.0, 4.0)
.push([(0, 0.75), (0, -0.75)])
.regularPolygon(0.5, 6, 90, mode="s")
)

result = cq.Workplane("front").placeSketch(sk1)
show_object(result)
23 changes: 23 additions & 0 deletions examples/produce_sketch_cadquery/sketch.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 4 additions & 2 deletions partcad/src/partcad/part_factory_build123d.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,12 @@ async def instantiate(self, part):
picklestring = pickle.dumps(request)
request_serialized = base64.b64encode(picklestring).decode()

await self.runtime.ensure("ocp-tessellate")
await self.runtime.ensure("cadquery")
await self.runtime.ensure("numpy==1.24.1")
await self.runtime.ensure("numpy-quaternion==2023.0.4")
await self.runtime.ensure("nptyping==1.24.1")
await self.runtime.ensure("cadquery")
await self.runtime.ensure("ocp-tessellate")
await self.runtime.ensure("typing_extensions>=4.6.0,<5")
await self.runtime.ensure("build123d")
cwd = self.project.config_dir
if self.cwd is not None:
Expand Down
6 changes: 4 additions & 2 deletions partcad/src/partcad/part_factory_cadquery.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,12 @@ async def instantiate(self, part):
picklestring = pickle.dumps(request)
request_serialized = base64.b64encode(picklestring).decode()

await self.runtime.ensure("ocp-tessellate")
await self.runtime.ensure("cadquery")
await self.runtime.ensure("numpy==1.24.1")
await self.runtime.ensure("numpy-quaternion==2023.0.4")
await self.runtime.ensure("nptyping==1.24.1")
await self.runtime.ensure("cadquery")
await self.runtime.ensure("ocp-tessellate")
await self.runtime.ensure("typing_extensions>=4.6.0,<5")
cwd = self.project.config_dir
if self.cwd is not None:
cwd = os.path.join(self.project.config_dir, self.cwd)
Expand Down
3 changes: 3 additions & 0 deletions partcad/src/partcad/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from .sketch_factory_dxf import SketchFactoryDxf
from .sketch_factory_svg import SketchFactorySvg
from .sketch_factory_build123d import SketchFactoryBuild123d
from .sketch_factory_cadquery import SketchFactoryCadquery
from . import part
from . import part_config
from .part_factory_extrude import PartFactoryExtrude
Expand Down Expand Up @@ -378,6 +379,8 @@ def init_sketch_by_config(self, config, source_project=None):
)
elif config["type"] == "build123d":
SketchFactoryBuild123d(self.ctx, source_project, self, config)
elif config["type"] == "cadquery":
SketchFactoryCadquery(self.ctx, source_project, self, config)
elif config["type"] == "dxf":
SketchFactoryDxf(self.ctx, source_project, self, config)
elif config["type"] == "svg":
Expand Down
3 changes: 2 additions & 1 deletion partcad/src/partcad/provider_factory_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,11 @@ async def query_script(self, provider, script_name, request):
picklestring = pickle.dumps(request)
request_serialized = base64.b64encode(picklestring).decode()

await self.runtime.ensure("ocp-tessellate")
await self.runtime.ensure("numpy==1.24.1")
await self.runtime.ensure("numpy-quaternion==2023.0.4")
await self.runtime.ensure("nptyping==1.24.1")
await self.runtime.ensure("cadquery")
await self.runtime.ensure("ocp-tessellate")
cwd = self.project.config_dir
if self.cwd is not None:
cwd = os.path.join(self.project.config_dir, self.cwd)
Expand Down
16 changes: 14 additions & 2 deletions partcad/src/partcad/sketch_factory_build123d.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ class SketchFactoryBuild123d(SketchFactoryPython):
def __init__(
self, ctx, source_project, target_project, config, can_create=False
):
python_version = source_project.python_version
if python_version is None:
# Stay one step ahead of the minimum required Python version
python_version = "3.10"
if python_version == "3.12" or python_version == "3.11":
pc_logging.debug(
"Downgrading Python version to 3.10 to avoid compatibility issues with build123d"
)
python_version = "3.10"
with pc_logging.Action(
"InitBuild123d", target_project.name, config["name"]
):
Expand All @@ -43,6 +52,7 @@ def __init__(
target_project,
config,
can_create=can_create,
python_version=python_version,
)
# Complement the config object here if necessary
self._create(config)
Expand Down Expand Up @@ -88,10 +98,12 @@ async def instantiate(self, sketch):
picklestring = pickle.dumps(request)
request_serialized = base64.b64encode(picklestring).decode()

await self.runtime.ensure("ocp-tessellate")
await self.runtime.ensure("cadquery")
await self.runtime.ensure("numpy==1.24.1")
await self.runtime.ensure("numpy-quaternion==2023.0.4")
await self.runtime.ensure("nptyping==1.24.1")
await self.runtime.ensure("cadquery")
await self.runtime.ensure("ocp-tessellate")
await self.runtime.ensure("typing_extensions>=4.6.0,<5")
await self.runtime.ensure("build123d")
cwd = self.project.config_dir
if self.cwd is not None:
Expand Down
188 changes: 188 additions & 0 deletions partcad/src/partcad/sketch_factory_cadquery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
#
# 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 OCP.gp import gp_Ax1
from OCP.TopoDS import (
TopoDS_Builder,
TopoDS_Compound,
TopoDS_Edge,
TopoDS_Wire,
TopoDS_Face,
)
from OCP.TopLoc import TopLoc_Location

from .sketch_factory_python import SketchFactoryPython
from . import wrapper
from . import logging as pc_logging

sys.path.append(os.path.join(os.path.dirname(__file__), "wrappers"))
from cq_serialize import register as register_cq_helper


class SketchFactoryCadquery(SketchFactoryPython):
def __init__(
self, ctx, source_project, target_project, config, can_create=False
):
python_version = source_project.python_version
if python_version is None:
# Stay one step ahead of the minimum required Python version
python_version = "3.10"
if python_version == "3.12" or python_version == "3.11":
pc_logging.debug(
"Downgrading Python version to 3.10 to avoid compatibility issues with CadQuery"
)
python_version = "3.10"
with pc_logging.Action(
"InitCadQuery", target_project.name, config["name"]
):
super().__init__(
ctx,
source_project,
target_project,
config,
can_create=can_create,
python_version=python_version,
)
# Complement the config object here if necessary
self._create(config)

async def instantiate(self, sketch):
await super().instantiate(sketch)

with pc_logging.Action("CadQuery", sketch.project_name, sketch.name):
if (
not os.path.exists(sketch.path)
or os.path.getsize(sketch.path) == 0
):
pc_logging.error(
"CadQuery script is empty or does not exist: %s"
% sketch.path
)
return None

# Finish initialization of PythonRuntime
# which was too expensive to do in the constructor
await self.prepare_python()

# Get the path to the wrapper script
# which needs to be executed
wrapper_path = wrapper.get("cadquery.py")

# Build the request
request = {"build_parameters": {}}
if "parameters" in self.config:
for param_name, param in self.config["parameters"].items():
request["build_parameters"][param_name] = param["default"]
patch = {}
if "patch" in self.config:
patch.update(self.config["patch"])
request["patch"] = patch

# Serialize the request
register_cq_helper()
picklestring = pickle.dumps(request)
request_serialized = base64.b64encode(picklestring).decode()

await self.runtime.ensure("ocp-tessellate")
await self.runtime.ensure("cadquery")
await self.runtime.ensure("numpy==1.24.1")
await self.runtime.ensure("numpy-quaternion==2023.0.4")
await self.runtime.ensure("nptyping==1.24.1")
await self.runtime.ensure("typing_extensions>=4.6.0,<5")
cwd = self.project.config_dir
if self.cwd is not None:
cwd = os.path.join(self.project.config_dir, self.cwd)
response_serialized, errors = await self.runtime.run(
[
wrapper_path,
os.path.abspath(sketch.path),
os.path.abspath(cwd),
],
request_serialized,
)
if len(errors) > 0:
error_lines = errors.split("\n")
for error_line in error_lines:
sketch.error("%s: %s" % (sketch.name, error_line))

try:
# pc_logging.error("Response: %s" % response_serialized)
response = base64.b64decode(response_serialized)
register_cq_helper()
result = pickle.loads(response)
except Exception as e:
sketch.error(
"Exception while deserializing %s: %s" % (sketch.name, e)
)
return None

if not result["success"]:
sketch.error("%s: %s" % (sketch.name, result["exception"]))
return None

self.ctx.stats_sketches_instantiated += 1

if result["shapes"] is None:
return None
if len(result["shapes"]) == 0:
return None
if len(result["shapes"]) == 1:
return result["shapes"][0]

builder = TopoDS_Builder()
compound = TopoDS_Compound()
builder.MakeCompound(compound)

def process(shapes, components_list):
for shape in shapes:
# pc_logging.info("Returned: %s" % type(shape))
try:
if shape is None or isinstance(shape, str):
# pc_logging.info("String: %s" % shape)
continue

if isinstance(shape, list):
child_component_list = list()
process(shape, child_component_list)
components_list.append(child_component_list)
continue

# TODO(clairbee): add support for the below types
if isinstance(shape, TopLoc_Location) or isinstance(
shape, gp_Ax1
):
continue

if (
isinstance(shape, TopoDS_Edge)
or isinstance(shape, TopoDS_Wire)
or isinstance(shape, TopoDS_Face)
):
builder.Add(compound, shape)
components_list.append(shape)
elif False:
# TODO(clairbee) Add all metadata types here
components_list.append(shape)
else:
pc_logging.error(
"Unsupported shape type: %s" % type(shape)
)
except Exception as e:
pc_logging.error(
"Error adding shape to compound: %s" % e
)

process(result["shapes"], sketch.components)

return compound
Loading

0 comments on commit 9037e90

Please sign in to comment.