Skip to content

Commit c646aee

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 c646aee

File tree

4 files changed

+127
-69
lines changed

4 files changed

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

tests/test_projection.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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), f"{prefix}{name}.dxf", doc_units=6,
19+
)
20+
21+
22+
def exportSVG3rdAngleProjection(my_part, prefix: str) -> None:
23+
for name, direction in viewpoint.items():
24+
exportSVG(
25+
my_part, f"{prefix}{name}.svg", opts={"projectionDir": direction,},
26+
)
27+
28+
29+
if __name__ == "__main__":
30+
# Build the part
31+
width = 10
32+
depth = 10
33+
height = 10
34+
35+
# !!! Test projection of fillets to arc segments in DXF. !!!
36+
baseplate = cq.Workplane("XY").box(width, depth, height).edges("|Z").fillet(2.0) #
37+
38+
hole_dia = 3.0
39+
40+
# !!! Test projection of countersunk to arc segments in DXF. !!!
41+
drilled = baseplate.faces(">Z").workplane().cskHole(hole_dia, hole_dia * 2, 82.0) #
42+
43+
# Expected DXF output to be identical to SVG output
44+
exportSVG3rdAngleProjection(drilled, "")
45+
exportDXF3rdAngleProjection(drilled, "")

0 commit comments

Comments
 (0)