Skip to content

Commit 97a6903

Browse files
committed
Project 3D part to plane
Simulate the API from Build123d `project_to_viewpoint`: Given a 3D vector representing the camera position pointing at the origin `(0,0,0)`, render all visible edges/arcs/splines representing the outline of the 3D part projected to the camera. Also render all hidden edges/arcs/splines. Provide and example script `tests/test_projection.py` where a generic part (A bracket with rounded corners and a countersunk hole) is projected to top view, side view, front view, and orthogonal view, and then exported to DXF 2D drawings. Reference: https://build123d.readthedocs.io/en/latest/tech_drawing_tutorial.html
1 parent 8a8b996 commit 97a6903

File tree

4 files changed

+150
-69
lines changed

4 files changed

+150
-69
lines changed

cadquery/occ_impl/exporters/dxf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ def add_shape(self, shape: Union[WorkplaneLike, Shape], layer: str = "") -> Self
157157
plane = shape.plane
158158
shape_ = compound(*shape.__iter__()).transformShape(plane.fG)
159159
else:
160+
plane = Plane((0,0,0))
160161
shape_ = shape
161162

162163
general_attributes = {}

cadquery/occ_impl/exporters/svg.py

Lines changed: 16 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
import io as StringIO
22

3-
from ..shapes import Shape, Compound, TOLERANCE
3+
from ..shapes import Shape, Compound, Edge
44
from ..geom import BoundBox
5+
from ..projection import projectToViewpoint
56

67

7-
from OCP.gp import gp_Ax2, gp_Pnt, gp_Dir
8-
from OCP.BRepLib import BRepLib
9-
from OCP.HLRBRep import HLRBRep_Algo, HLRBRep_HLRToShape
10-
from OCP.HLRAlgo import HLRAlgo_Projector
118
from OCP.GCPnts import GCPnts_QuasiUniformDeflection
129

1310
DISCRETIZATION_TOLERANCE = 1e-3
@@ -106,26 +103,24 @@ def makeSVGedge(e):
106103
return cs.getvalue()
107104

108105

109-
def getPaths(visibleShapes, hiddenShapes):
106+
def getPaths(visibleEdges: list[Edge], hiddenEdges: list[Edge]) -> tuple[list[str], list[str]]:
110107
"""
111108
Collects the visible and hidden edges from the CadQuery object.
112109
"""
113110

114111
hiddenPaths = []
115112
visiblePaths = []
116113

117-
for s in visibleShapes:
118-
for e in s.Edges():
119-
visiblePaths.append(makeSVGedge(e))
114+
for e in visibleEdges:
115+
visiblePaths.append(makeSVGedge(e))
120116

121-
for s in hiddenShapes:
122-
for e in s.Edges():
123-
hiddenPaths.append(makeSVGedge(e))
117+
for e in hiddenEdges:
118+
hiddenPaths.append(makeSVGedge(e))
124119

125120
return (hiddenPaths, visiblePaths)
126121

127122

128-
def getSVG(shape, opts=None):
123+
def getSVG(shape: Shape, opts=None):
129124
"""
130125
Export a shape to SVG text.
131126
@@ -171,10 +166,10 @@ def getSVG(shape, opts=None):
171166

172167
# Handle the case where the height or width are None
173168
width = d["width"]
174-
if width != None:
169+
if width is not None:
175170
width = float(d["width"])
176171
height = d["height"]
177-
if d["height"] != None:
172+
if d["height"] is not None:
178173
height = float(d["height"])
179174
marginLeft = float(d["marginLeft"])
180175
marginTop = float(d["marginTop"])
@@ -184,66 +179,18 @@ def getSVG(shape, opts=None):
184179
strokeColor = tuple(d["strokeColor"])
185180
hiddenColor = tuple(d["hiddenColor"])
186181
showHidden = bool(d["showHidden"])
187-
focus = float(d["focus"]) if d.get("focus") else None
182+
focus = float(d["focus"]) if d.get("focus") is not None else None
188183

189-
hlr = HLRBRep_Algo()
190-
hlr.Add(shape.wrapped)
191-
192-
coordinate_system = gp_Ax2(gp_Pnt(), gp_Dir(*projectionDir))
193-
194-
if focus is not None:
195-
projector = HLRAlgo_Projector(coordinate_system, focus)
196-
else:
197-
projector = HLRAlgo_Projector(coordinate_system)
198-
199-
hlr.Projector(projector)
200-
hlr.Update()
201-
hlr.Hide()
202-
203-
hlr_shapes = HLRBRep_HLRToShape(hlr)
204-
205-
visible = []
206-
207-
visible_sharp_edges = hlr_shapes.VCompound()
208-
if not visible_sharp_edges.IsNull():
209-
visible.append(visible_sharp_edges)
210-
211-
visible_smooth_edges = hlr_shapes.Rg1LineVCompound()
212-
if not visible_smooth_edges.IsNull():
213-
visible.append(visible_smooth_edges)
214-
215-
visible_contour_edges = hlr_shapes.OutLineVCompound()
216-
if not visible_contour_edges.IsNull():
217-
visible.append(visible_contour_edges)
218-
219-
hidden = []
220-
221-
hidden_sharp_edges = hlr_shapes.HCompound()
222-
if not hidden_sharp_edges.IsNull():
223-
hidden.append(hidden_sharp_edges)
224-
225-
hidden_contour_edges = hlr_shapes.OutLineHCompound()
226-
if not hidden_contour_edges.IsNull():
227-
hidden.append(hidden_contour_edges)
228-
229-
# Fix the underlying geometry - otherwise we will get segfaults
230-
for el in visible:
231-
BRepLib.BuildCurves3d_s(el, TOLERANCE)
232-
for el in hidden:
233-
BRepLib.BuildCurves3d_s(el, TOLERANCE)
234-
235-
# convert to native CQ objects
236-
visible = list(map(Shape, visible))
237-
hidden = list(map(Shape, hidden))
238-
(hiddenPaths, visiblePaths) = getPaths(visible, hidden)
184+
visibleEdges, hiddenEdges = projectToViewpoint(shape, projectionDir, focus)
185+
(hiddenPaths, visiblePaths) = getPaths(visibleEdges, hiddenEdges)
239186

240187
# get bounding box -- these are all in 2D space
241-
bb = Compound.makeCompound(hidden + visible).BoundingBox()
188+
bb = Compound.makeCompound(hiddenEdges + visibleEdges).BoundingBox()
242189

243190
# Determine whether the user wants to fit the drawing to the bounding box
244-
if width == None or height == None:
191+
if width is None or height is None:
245192
# Fit image to specified width (or height)
246-
if width == None:
193+
if width is None:
247194
width = (height - (2.0 * marginTop)) * (
248195
bb.xlen / bb.ylen
249196
) + 2.0 * marginLeft

cadquery/occ_impl/projection.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from typing import Optional
2+
3+
from OCP.BRepLib import BRepLib
4+
from OCP.gp import gp_Ax2, gp_Dir, gp_Pnt
5+
from OCP.HLRAlgo import HLRAlgo_Projector
6+
from OCP.HLRBRep import HLRBRep_Algo, HLRBRep_HLRToShape
7+
8+
from .shapes import TOLERANCE, Edge, Shape
9+
10+
DISCRETIZATION_TOLERANCE = 1e-3
11+
12+
13+
def projectToViewpoint(
14+
shape,
15+
projectionDir: tuple[float, float, float],
16+
focus: Optional[float] = None,
17+
) -> tuple[list[Edge], list[Edge]]:
18+
hlr = HLRBRep_Algo()
19+
hlr.Add(shape.wrapped)
20+
21+
coordinate_system = gp_Ax2(gp_Pnt(), gp_Dir(*projectionDir))
22+
23+
if focus is not None:
24+
projector = HLRAlgo_Projector(coordinate_system, focus)
25+
else:
26+
projector = HLRAlgo_Projector(coordinate_system)
27+
28+
hlr.Projector(projector)
29+
hlr.Update()
30+
hlr.Hide()
31+
32+
hlr_shapes = HLRBRep_HLRToShape(hlr)
33+
34+
visible = []
35+
36+
visible_sharp_edges = hlr_shapes.VCompound()
37+
if not visible_sharp_edges.IsNull():
38+
visible.append(visible_sharp_edges)
39+
40+
visible_smooth_edges = hlr_shapes.Rg1LineVCompound()
41+
if not visible_smooth_edges.IsNull():
42+
visible.append(visible_smooth_edges)
43+
44+
visible_contour_edges = hlr_shapes.OutLineVCompound()
45+
if not visible_contour_edges.IsNull():
46+
visible.append(visible_contour_edges)
47+
48+
hidden = []
49+
50+
hidden_sharp_edges = hlr_shapes.HCompound()
51+
if not hidden_sharp_edges.IsNull():
52+
hidden.append(hidden_sharp_edges)
53+
54+
hidden_contour_edges = hlr_shapes.OutLineHCompound()
55+
if not hidden_contour_edges.IsNull():
56+
hidden.append(hidden_contour_edges)
57+
58+
# Fix the underlying geometry - otherwise we will get segfaults
59+
for el in visible:
60+
BRepLib.BuildCurves3d_s(el, TOLERANCE)
61+
for el in hidden:
62+
BRepLib.BuildCurves3d_s(el, TOLERANCE)
63+
64+
# convert to native CQ objects
65+
visible = [Shape.cast(s) for s in visible] # s is a TopoDS_Shape (Compound)
66+
hidden = [Shape.cast(s) for s in hidden]
67+
68+
# Extract edges
69+
visible_edges = [e for c in visible for e in c.Edges()]
70+
hidden_edges = [e for c in hidden for e in c.Edges()]
71+
72+
return visible_edges, hidden_edges

tests/test_projection.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import cadquery as cq
2+
from cadquery.occ_impl.projection import projectToViewpoint
3+
from cadquery.occ_impl.exporters.svg import exportSVG
4+
from cadquery.occ_impl.shapes import Compound
5+
from cadquery import Workplane
6+
7+
viewpoint = {
8+
"top": (0, 0, 1),
9+
"left": (1, 0, 0),
10+
"front": (0, 1, 0),
11+
"ortho": (1, 1, 1),
12+
}
13+
14+
15+
def exportDXF3rdAngleProjection(my_part: Workplane, prefix: str) -> None:
16+
for name, direction in viewpoint.items():
17+
visible_edges, hidden_edges = projectToViewpoint(my_part.val(), direction)
18+
cq.exporters.exportDXF(
19+
Compound.makeCompound(visible_edges),
20+
f"{prefix}{name}.dxf",
21+
doc_units=6,
22+
)
23+
24+
25+
def exportSVG3rdAngleProjection(my_part, prefix: str) -> None:
26+
for name, direction in viewpoint.items():
27+
exportSVG(
28+
my_part,
29+
f"{prefix}{name}.svg",
30+
opts={
31+
"projectionDir": direction,
32+
},
33+
)
34+
35+
36+
if __name__ == "__main__":
37+
# Build the part
38+
width = 10
39+
depth = 10
40+
height = 10
41+
42+
# !!! Test projection of fillets to arc segments in DXF. !!!
43+
baseplate = (
44+
cq.Workplane("XY") #
45+
.box(width, depth, height)
46+
.edges("|Z")
47+
.fillet(2.0)
48+
)
49+
50+
hole_dia = 3.0
51+
52+
# !!! Test projection of countersunk to arc segments in DXF. !!!
53+
drilled = (
54+
baseplate.faces(">Z") #
55+
.workplane()
56+
.cskHole(hole_dia, hole_dia * 2, 82.0)
57+
)
58+
59+
# Expected DXF output to be identical to SVG output
60+
exportSVG3rdAngleProjection(drilled, "")
61+
exportDXF3rdAngleProjection(drilled, "")

0 commit comments

Comments
 (0)