Skip to content

Commit a555328

Browse files
authored
Merge branch 'master' into master
2 parents 273507a + d5806e3 commit a555328

22 files changed

+521
-166
lines changed

.coveragerc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ branch = True
33
omit =
44
cadquery/utils.py
55
cadquery/cq_directive.py
6+
tests/*
67

78
[report]
89
exclude_lines =

README.md

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ CadQuery is often compared to [OpenSCAD](http://www.openscad.org/). Like OpenSCA
2525
2. CadQuery's CAD kernel Open CASCADE Technology ([OCCT](https://en.wikipedia.org/wiki/Open_Cascade_Technology)) is much more powerful than the [CGAL](https://en.wikipedia.org/wiki/CGAL) used by OpenSCAD. Features supported natively by OCCT include NURBS, splines, surface sewing, STL repair, STEP import/export, and other complex operations, in addition to the standard CSG operations supported by CGAL
2626
3. Ability to import/export [STEP](https://en.wikipedia.org/wiki/ISO_10303) and the ability to begin with a STEP model, created in a CAD package, and then add parametric features. This is possible in OpenSCAD using STL, but STL is a lossy format.
2727
4. CadQuery scripts require less code to create most objects, because it is possible to locate features based on the position of other features, workplanes, vertices, etc.
28-
5. CadQuery scripts can build STL, STEP, and AMF faster than OpenSCAD.
28+
5. CadQuery scripts can build STL, STEP, AMF and 3MF faster than OpenSCAD.
2929

3030
### Key features
3131
* Build 3D models with scripts that are as close as possible to how you would describe the object to a human.
3232
* Create parametric models that can be very easily customized by end users.
33-
* Output high quality (loss-less) CAD formats like STEP and DXF in addition to STL, VRML and AMF.
33+
* Output high quality (loss-less) CAD formats like STEP and DXF in addition to STL, VRML, AMF and 3MF.
3434
* Provide a non-proprietary, plain text model format that can be edited and executed with only a web browser.
3535
* Offer advanced modeling capabilities such as fillets, curvilinear extrudes, parametric curves and lofts.
3636
* Build nested assemblies out of individual parts and other assemblies.
@@ -53,19 +53,8 @@ There are currently 4 different ways to use CadQuery for designing your next pro
5353
* Linux [installation video](https://youtu.be/sjLTePOq8bQ)
5454
* Windows [installation video](https://youtu.be/3Tg_RJhqZRg)
5555

56-
There are two ways to install the CadQuery library and its dependencies. One is using conda, and the other is using pip. Pip is shown first below, followed by two sections on installing CadQuery via conda, and a non-intrusive way to install conda on a system.
5756

58-
### CQ-editor GUI
59-
60-
CQ-editor is an IDE that allows users to edit CadQuery model scripts in a GUI environment. It includes features such as:
61-
62-
* A graphical debugger that allows you to step through your scripts.
63-
* A CadQuery stack inspector.
64-
* Export to various formats, including STEP and STL, directly from the menu.
65-
66-
The installation instructions for CQ-editor can be found [here](https://github.com/CadQuery/CQ-editor#installation).
67-
68-
<img src="https://raw.githubusercontent.com/CadQuery/CQ-editor/master/screenshots/screenshot3.png" alt="CQ editor screenshot" width="800"/>
57+
There are two ways to install CadQuery and its dependencies. One is using conda, and the other is using pip. Pip is shown first below, followed by two sections on installing CadQuery via conda, and a non-intrusive way to install conda on a system. Note that conda is the better supported option.
6958

7059
### CadQuery Installation Via Pip
7160

@@ -77,9 +66,9 @@ python3 -m pip install --upgrade pip
7766
```
7867
Once a current version of pip is installed, CadQuery can be installed using the following command line.
7968
```
80-
pip install cadquery==2.2.0b0
69+
pip install --pre cadquery
8170
```
82-
Notice that the version number is pinned to a beta version. This is because CadQuery has only recently returned to PyPI, and a full release has not happened yet. When the final release of CadQuery 2.2 is published, it will be possible to simply type `pip install cadquery`. However, if the version number pin is not used for now, a 1.x version of CadQuery will be installed, which is extremely out of date.
71+
NB: CadQuery has only recently returned to PyPI, and a full release has not happened yet. When the final release of CadQuery 2.2 is published, it will be possible to simply type `pip install cadquery`.
8372

8473
It is also possible to install the very latest changes directly from CadQuery's GitHub repository, with the understanding that sometimes breaking changes can occur. To install from the git repository, run the following command line.
8574
```

appveyor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ install:
2828
- cmd: conda init cmd.exe
2929
- conda env create -f environment.yml
3030
- conda activate cadquery
31-
- pip install git+https://github.com/CadQuery/OCP-stubs.git@7.5.1
31+
- pip install git+https://github.com/CadQuery/OCP-stubs.git@7.6.3
3232

3333
build: false
3434

cadquery/cq.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,7 @@ def workplane(
530530
:type invert: boolean or None=False
531531
:type centerOption: string or None='ProjectedOrigin'
532532
:type origin: Vector or None
533-
:rtype: Workplane object
533+
:rtype: Workplane object
534534
535535
The first element on the stack must be a face, a set of
536536
co-planar faces or a vertex. If a vertex, then the parent
@@ -3019,7 +3019,7 @@ def extrude(
30193019
passed, the extrusion extends this far and a negative value is in the opposite direction
30203020
to the normal of the plane. The string "next" extrudes until the next face orthogonal to
30213021
the wire normal. "last" extrudes to the last face. If a object of type Face is passed then
3022-
the extrusion will extend until this face. **Note that the Workplane must contain a Solid for extruding to a given face.**
3022+
the extrusion will extend until this face. **Note that the Workplane must contain a Solid for extruding to a given face.**
30233023
:param combine: True or "a" to combine the resulting solid with parent solids if found, "cut" or "s" to remove the resulting solid from the parent solids if found. False to keep the resulting solid separated from the parent solids.
30243024
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
30253025
:param boolean both: extrude in both directions symmetrically
@@ -3524,7 +3524,7 @@ def loft(
35243524
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
35253525
35263526
:return: a Workplane object containing the created loft
3527-
3527+
35283528
"""
35293529

35303530
if self.ctx.pendingWires:
@@ -3765,7 +3765,7 @@ def interpPlate(
37653765
) -> T:
37663766
"""
37673767
Returns a plate surface that is 'thickness' thick, enclosed by 'surf_edge_pts' points, and going
3768-
through 'surf_pts' points. Using pushPoints directly with interpPlate and combine=True, can be
3768+
through 'surf_pts' points. Using pushPoints directly with interpPlate and combine=True, can be
37693769
very resources intensive depending on the complexity of the shape. In this case set combine=False.
37703770
37713771
:param surf_edges
@@ -3833,7 +3833,7 @@ def interpPlate(
38333833
# thicken if needed
38343834
s = f.thicken(thickness) if thickness > 0 else f
38353835

3836-
return self.eachpoint(lambda loc: s.moved(loc), True, combine)
3836+
return self.eachpoint(lambda loc: s.moved(loc), True, combine, clean)
38373837

38383838
def box(
38393839
self: T,
@@ -3903,7 +3903,7 @@ def box(
39033903

39043904
box = Solid.makeBox(length, width, height, offset)
39053905

3906-
return self.eachpoint(lambda loc: box.moved(loc), True, combine)
3906+
return self.eachpoint(lambda loc: box.moved(loc), True, combine, clean)
39073907

39083908
def sphere(
39093909
self: T,
@@ -3967,7 +3967,7 @@ def sphere(
39673967
s = Solid.makeSphere(radius, offset, direct, angle1, angle2, angle3)
39683968

39693969
# We want a sphere for each point on the workplane
3970-
return self.eachpoint(lambda loc: s.moved(loc), True, combine)
3970+
return self.eachpoint(lambda loc: s.moved(loc), True, combine, clean)
39713971

39723972
def cylinder(
39733973
self: T,
@@ -4024,7 +4024,7 @@ def cylinder(
40244024
s = Solid.makeCylinder(radius, height, offset, direct, angle)
40254025

40264026
# We want a cylinder for each point on the workplane
4027-
return self.eachpoint(lambda loc: s.moved(loc), True, combine)
4027+
return self.eachpoint(lambda loc: s.moved(loc), True, combine, clean)
40284028

40294029
def wedge(
40304030
self: T,
@@ -4094,7 +4094,7 @@ def wedge(
40944094
w = Solid.makeWedge(dx, dy, dz, xmin, zmin, xmax, zmax, offset, dir)
40954095

40964096
# We want a wedge for each point on the workplane
4097-
return self.eachpoint(lambda loc: w.moved(loc), True, combine)
4097+
return self.eachpoint(lambda loc: w.moved(loc), True, combine, clean)
40984098

40994099
def clean(self: T) -> T:
41004100
"""

cadquery/occ_impl/exporters/__init__.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from .svg import getSVG
1515
from .json import JsonMesh
1616
from .amf import AmfWriter
17+
from .threemf import ThreeMFWriter
1718
from .dxf import exportDXF
1819
from .vtk import exportVTP
1920
from .utils import toCompound
@@ -28,9 +29,12 @@ class ExportTypes:
2829
DXF = "DXF"
2930
VRML = "VRML"
3031
VTP = "VTP"
32+
THREEMF = "3MF"
3133

3234

33-
ExportLiterals = Literal["STL", "STEP", "AMF", "SVG", "TJS", "DXF", "VRML", "VTP"]
35+
ExportLiterals = Literal[
36+
"STL", "STEP", "AMF", "SVG", "TJS", "DXF", "VRML", "VTP", "3MF"
37+
]
3438

3539

3640
def export(
@@ -93,6 +97,11 @@ def export(
9397
with open(fname, "wb") as f:
9498
aw.writeAmf(f)
9599

100+
elif exportType == ExportTypes.THREEMF:
101+
tmfw = ThreeMFWriter(shape, tolerance, angularTolerance, **opt or {})
102+
with open(fname, "wb") as f:
103+
tmfw.write3mf(f)
104+
96105
elif exportType == ExportTypes.DXF:
97106
if isinstance(w, Workplane):
98107
exportDXF(w, fname)
@@ -106,7 +115,12 @@ def export(
106115
shape.exportStep(fname)
107116

108117
elif exportType == ExportTypes.STL:
109-
shape.exportStl(fname, tolerance, angularTolerance)
118+
if opt:
119+
useascii = opt.get("ascii", False) or opt.get("ASCII", False)
120+
else:
121+
useascii = False
122+
123+
shape.exportStl(fname, tolerance, angularTolerance, useascii)
110124

111125
elif exportType == ExportTypes.VRML:
112126
shape.mesh(tolerance, angularTolerance)
@@ -175,6 +189,9 @@ def tessellate(shape, angularTolerance):
175189
tess = tessellate(shape, angularTolerance)
176190
aw = AmfWriter(tess)
177191
aw.writeAmf(fileLike)
192+
elif exportType == ExportTypes.THREEMF:
193+
tmfw = ThreeMFWriter(shape, tolerance, angularTolerance)
194+
tmfw.write3mf(fileLike)
178195
else:
179196

180197
# all these types required writing to a file and then
@@ -187,7 +204,7 @@ def tessellate(shape, angularTolerance):
187204
if exportType == ExportTypes.STEP:
188205
shape.exportStep(outFileName)
189206
elif exportType == ExportTypes.STL:
190-
shape.exportStl(outFileName, tolerance, angularTolerance)
207+
shape.exportStl(outFileName, tolerance, angularTolerance, True)
191208
else:
192209
raise ValueError("No idea how i got here")
193210

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
from datetime import datetime
2+
from os import PathLike
3+
import xml.etree.cElementTree as ET
4+
from typing import IO, List, Literal, Tuple, Union
5+
from zipfile import ZipFile, ZIP_DEFLATED, ZIP_STORED
6+
7+
from ...cq import Compound, Shape, Vector
8+
9+
10+
class CONTENT_TYPES(object):
11+
MODEL = "application/vnd.ms-package.3dmanufacturing-3dmodel+xml"
12+
RELATION = "application/vnd.openxmlformats-package.relationships+xml"
13+
14+
15+
class SCHEMAS(object):
16+
CONTENT_TYPES = "http://schemas.openxmlformats.org/package/2006/content-types"
17+
RELATION = "http://schemas.openxmlformats.org/package/2006/relationships"
18+
CORE = "http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
19+
MODEL = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"
20+
21+
22+
Unit = Literal["micron", "millimeter", "centimeter", "meter", "inch", "foot"]
23+
24+
25+
class ThreeMFWriter(object):
26+
def __init__(
27+
self,
28+
shape: Shape,
29+
tolerance: float,
30+
angularTolerance: float,
31+
unit: Unit = "millimeter",
32+
):
33+
"""
34+
Initialize the writer.
35+
Used to write the given Shape to a 3MF file.
36+
"""
37+
self.unit = unit
38+
39+
if isinstance(shape, Compound):
40+
shapes = list(shape)
41+
else:
42+
shapes = [shape]
43+
44+
tessellations = [s.tessellate(tolerance, angularTolerance) for s in shapes]
45+
# Remove shapes that did not tesselate
46+
self.tessellations = [t for t in tessellations if all(t)]
47+
48+
def write3mf(
49+
self, outfile: Union[PathLike, str, IO[bytes]],
50+
):
51+
"""
52+
Write to the given file.
53+
"""
54+
55+
try:
56+
import zlib
57+
58+
compression = ZIP_DEFLATED
59+
except ImportError:
60+
compression = ZIP_STORED
61+
62+
with ZipFile(outfile, "w", compression) as zf:
63+
zf.writestr("_rels/.rels", self._write_relationships())
64+
zf.writestr("[Content_Types].xml", self._write_content_types())
65+
zf.writestr("3D/3dmodel.model", self._write_3d())
66+
67+
def _write_3d(self) -> str:
68+
69+
no_meshes = len(self.tessellations)
70+
71+
model = ET.Element(
72+
"model", {"xml:lang": "en-US", "xmlns": SCHEMAS.CORE,}, unit=self.unit,
73+
)
74+
75+
# Add meta data
76+
ET.SubElement(
77+
model, "metadata", name="Application"
78+
).text = "CadQuery 3MF Exporter"
79+
ET.SubElement(
80+
model, "metadata", name="CreationDate"
81+
).text = datetime.now().isoformat()
82+
83+
resources = ET.SubElement(model, "resources")
84+
85+
# Add all meshes to resources
86+
for i, tessellation in enumerate(self.tessellations):
87+
self._add_mesh(resources, str(i), tessellation)
88+
89+
# Create a component of all meshes
90+
comp_object = ET.SubElement(
91+
resources,
92+
"object",
93+
id=str(no_meshes),
94+
name=f"CadQuery Component",
95+
type="model",
96+
)
97+
components = ET.SubElement(comp_object, "components")
98+
99+
# Add all meshes to the component
100+
for i in range(no_meshes):
101+
ET.SubElement(
102+
components, "component", objectid=str(i),
103+
)
104+
105+
# Add the component to the build
106+
build = ET.SubElement(model, "build")
107+
ET.SubElement(build, "item", objectid=str(no_meshes))
108+
109+
return ET.tostring(model, xml_declaration=True, encoding="utf-8")
110+
111+
def _add_mesh(
112+
self,
113+
to: ET.Element,
114+
id: str,
115+
tessellation: Tuple[List[Vector], List[Tuple[int, int, int]]],
116+
):
117+
object = ET.SubElement(
118+
to, "object", id=id, name=f"CadQuery Shape {id}", type="model"
119+
)
120+
mesh = ET.SubElement(object, "mesh")
121+
122+
# add vertices
123+
vertices = ET.SubElement(mesh, "vertices")
124+
for v in tessellation[0]:
125+
ET.SubElement(vertices, "vertex", x=str(v.x), y=str(v.y), z=str(v.z))
126+
127+
# add triangles
128+
volume = ET.SubElement(mesh, "triangles")
129+
for t in tessellation[1]:
130+
ET.SubElement(volume, "triangle", v1=str(t[0]), v2=str(t[1]), v3=str(t[2]))
131+
132+
def _write_content_types(self) -> str:
133+
134+
root = ET.Element("Types")
135+
root.set("xmlns", SCHEMAS.CONTENT_TYPES)
136+
ET.SubElement(
137+
root,
138+
"Override",
139+
PartName="/3D/3dmodel.model",
140+
ContentType=CONTENT_TYPES.MODEL,
141+
)
142+
ET.SubElement(
143+
root,
144+
"Override",
145+
PartName="/_rels/.rels",
146+
ContentType=CONTENT_TYPES.RELATION,
147+
)
148+
149+
return ET.tostring(root, xml_declaration=True, encoding="utf-8")
150+
151+
def _write_relationships(self) -> str:
152+
153+
root = ET.Element("Relationships")
154+
root.set("xmlns", SCHEMAS.RELATION)
155+
ET.SubElement(
156+
root,
157+
"Relationship",
158+
Target="/3D/3dmodel.model",
159+
Id="rel-1",
160+
Type=SCHEMAS.MODEL,
161+
TargetMode="Internal",
162+
)
163+
164+
return ET.tostring(root, xml_declaration=True, encoding="utf-8")

cadquery/occ_impl/jupyter_tools.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
const mapper = vtk.Rendering.Core.vtkMapper.newInstance();
3737
mapper.setInputConnection(reader.getOutputPort());
3838
mapper.setResolveCoincidentTopologyToPolygonOffset();
39-
mapper.setResolveCoincidentTopologyPolygonOffsetParameters(0.5,100);
39+
mapper.setResolveCoincidentTopologyPolygonOffsetParameters(0.9,20);
4040
4141
const actor = vtk.Rendering.Core.vtkActor.newInstance();
4242
actor.setMapper(mapper);

0 commit comments

Comments
 (0)