Skip to content

Commit d975b18

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 d975b18

4 files changed

Lines changed: 141 additions & 69 deletions

File tree

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 ..shapes 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/shapes.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,9 @@
158158
BRepAlgoAPI_Check,
159159
)
160160

161+
from OCP.HLRAlgo import HLRAlgo_Projector
162+
from OCP.HLRBRep import HLRBRep_Algo, HLRBRep_HLRToShape
163+
161164
from OCP.Geom import (
162165
Geom_BezierCurve,
163166
Geom_ConicalSurface,
@@ -6572,3 +6575,64 @@ def closest(s1: Shape, s2: Shape) -> Tuple[Vector, Vector]:
65726575
assert ext.Perform()
65736576

65746577
return Vector(ext.PointOnShape1(1)), Vector(ext.PointOnShape2(1))
6578+
6579+
def projectToViewpoint(
6580+
shape,
6581+
projectionDir: tuple[float, float, float],
6582+
focus: Optional[float] = None,
6583+
) -> tuple[list[Edge], list[Edge]]:
6584+
hlr = HLRBRep_Algo()
6585+
hlr.Add(shape.wrapped)
6586+
6587+
coordinate_system = gp_Ax2(gp_Pnt(), gp_Dir(*projectionDir))
6588+
6589+
if focus is not None:
6590+
projector = HLRAlgo_Projector(coordinate_system, focus)
6591+
else:
6592+
projector = HLRAlgo_Projector(coordinate_system)
6593+
6594+
hlr.Projector(projector)
6595+
hlr.Update()
6596+
hlr.Hide()
6597+
6598+
hlr_shapes = HLRBRep_HLRToShape(hlr)
6599+
6600+
visible = []
6601+
6602+
visible_sharp_edges = hlr_shapes.VCompound()
6603+
if not visible_sharp_edges.IsNull():
6604+
visible.append(visible_sharp_edges)
6605+
6606+
visible_smooth_edges = hlr_shapes.Rg1LineVCompound()
6607+
if not visible_smooth_edges.IsNull():
6608+
visible.append(visible_smooth_edges)
6609+
6610+
visible_contour_edges = hlr_shapes.OutLineVCompound()
6611+
if not visible_contour_edges.IsNull():
6612+
visible.append(visible_contour_edges)
6613+
6614+
hidden = []
6615+
6616+
hidden_sharp_edges = hlr_shapes.HCompound()
6617+
if not hidden_sharp_edges.IsNull():
6618+
hidden.append(hidden_sharp_edges)
6619+
6620+
hidden_contour_edges = hlr_shapes.OutLineHCompound()
6621+
if not hidden_contour_edges.IsNull():
6622+
hidden.append(hidden_contour_edges)
6623+
6624+
# Fix the underlying geometry - otherwise we will get segfaults
6625+
for el in visible:
6626+
BRepLib.BuildCurves3d_s(el, TOLERANCE)
6627+
for el in hidden:
6628+
BRepLib.BuildCurves3d_s(el, TOLERANCE)
6629+
6630+
# convert to native CQ objects
6631+
visible = [Shape.cast(s) for s in visible] # s is a TopoDS_Shape (Compound)
6632+
hidden = [Shape.cast(s) for s in hidden]
6633+
6634+
# Extract edges
6635+
visible_edges = [e for c in visible for e in c.Edges()]
6636+
hidden_edges = [e for c in hidden for e in c.Edges()]
6637+
6638+
return visible_edges, hidden_edges

tests/test_projection.py

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

0 commit comments

Comments
 (0)