Skip to content

Commit 8de8187

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 8de8187

File tree

4 files changed

+144
-69
lines changed

4 files changed

+144
-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: 18 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,26 @@ def makeSVGedge(e):
106103
return cs.getvalue()
107104

108105

109-
def getPaths(visibleShapes, hiddenShapes):
106+
def getPaths(
107+
visibleEdges: list[Edge], hiddenEdges: list[Edge]
108+
) -> tuple[list[str], list[str]]:
110109
"""
111110
Collects the visible and hidden edges from the CadQuery object.
112111
"""
113112

114113
hiddenPaths = []
115114
visiblePaths = []
116115

117-
for s in visibleShapes:
118-
for e in s.Edges():
119-
visiblePaths.append(makeSVGedge(e))
116+
for e in visibleEdges:
117+
visiblePaths.append(makeSVGedge(e))
120118

121-
for s in hiddenShapes:
122-
for e in s.Edges():
123-
hiddenPaths.append(makeSVGedge(e))
119+
for e in hiddenEdges:
120+
hiddenPaths.append(makeSVGedge(e))
124121

125122
return (hiddenPaths, visiblePaths)
126123

127124

128-
def getSVG(shape, opts=None):
125+
def getSVG(shape: Shape, opts=None):
129126
"""
130127
Export a shape to SVG text.
131128
@@ -171,10 +168,10 @@ def getSVG(shape, opts=None):
171168

172169
# Handle the case where the height or width are None
173170
width = d["width"]
174-
if width != None:
171+
if width is not None:
175172
width = float(d["width"])
176173
height = d["height"]
177-
if d["height"] != None:
174+
if d["height"] is not None:
178175
height = float(d["height"])
179176
marginLeft = float(d["marginLeft"])
180177
marginTop = float(d["marginTop"])
@@ -184,66 +181,18 @@ def getSVG(shape, opts=None):
184181
strokeColor = tuple(d["strokeColor"])
185182
hiddenColor = tuple(d["hiddenColor"])
186183
showHidden = bool(d["showHidden"])
187-
focus = float(d["focus"]) if d.get("focus") else None
184+
focus = float(d["focus"]) if d.get("focus") is not None else None
188185

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)
186+
visibleEdges, hiddenEdges = projectToViewpoint(shape, projectionDir, focus)
187+
hiddenPaths, visiblePaths = getPaths(visibleEdges, hiddenEdges)
239188

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

243192
# Determine whether the user wants to fit the drawing to the bounding box
244-
if width == None or height == None:
193+
if width is None or height is None:
245194
# Fit image to specified width (or height)
246-
if width == None:
195+
if width is None:
247196
width = (height - (2.0 * marginTop)) * (
248197
bb.xlen / bb.ylen
249198
) + 2.0 * marginLeft

cadquery/occ_impl/shapes.py

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