diff --git a/cadquery/occ_impl/exporters/dxf.py b/cadquery/occ_impl/exporters/dxf.py index 41ae15224..4a416f016 100644 --- a/cadquery/occ_impl/exporters/dxf.py +++ b/cadquery/occ_impl/exporters/dxf.py @@ -157,6 +157,7 @@ def add_shape(self, shape: Union[WorkplaneLike, Shape], layer: str = "") -> Self plane = shape.plane shape_ = compound(*shape.__iter__()).transformShape(plane.fG) else: + plane = Plane((0,0,0)) shape_ = shape general_attributes = {} diff --git a/cadquery/occ_impl/exporters/svg.py b/cadquery/occ_impl/exporters/svg.py index b41dee7b0..225909c74 100644 --- a/cadquery/occ_impl/exporters/svg.py +++ b/cadquery/occ_impl/exporters/svg.py @@ -1,13 +1,10 @@ import io as StringIO -from ..shapes import Shape, Compound, TOLERANCE +from ..shapes import Shape, Compound, Edge from ..geom import BoundBox +from ..projection import projectToViewpoint -from OCP.gp import gp_Ax2, gp_Pnt, gp_Dir -from OCP.BRepLib import BRepLib -from OCP.HLRBRep import HLRBRep_Algo, HLRBRep_HLRToShape -from OCP.HLRAlgo import HLRAlgo_Projector from OCP.GCPnts import GCPnts_QuasiUniformDeflection DISCRETIZATION_TOLERANCE = 1e-3 @@ -106,7 +103,7 @@ def makeSVGedge(e): return cs.getvalue() -def getPaths(visibleShapes, hiddenShapes): +def getPaths(visibleEdges: list[Edge], hiddenEdges: list[Edge]) -> tuple[list[str], list[str]]: """ Collects the visible and hidden edges from the CadQuery object. """ @@ -114,18 +111,16 @@ def getPaths(visibleShapes, hiddenShapes): hiddenPaths = [] visiblePaths = [] - for s in visibleShapes: - for e in s.Edges(): - visiblePaths.append(makeSVGedge(e)) + for e in visibleEdges: + visiblePaths.append(makeSVGedge(e)) - for s in hiddenShapes: - for e in s.Edges(): - hiddenPaths.append(makeSVGedge(e)) + for e in hiddenEdges: + hiddenPaths.append(makeSVGedge(e)) return (hiddenPaths, visiblePaths) -def getSVG(shape, opts=None): +def getSVG(shape: Shape, opts=None): """ Export a shape to SVG text. @@ -171,10 +166,10 @@ def getSVG(shape, opts=None): # Handle the case where the height or width are None width = d["width"] - if width != None: + if width is not None: width = float(d["width"]) height = d["height"] - if d["height"] != None: + if d["height"] is not None: height = float(d["height"]) marginLeft = float(d["marginLeft"]) marginTop = float(d["marginTop"]) @@ -184,66 +179,18 @@ def getSVG(shape, opts=None): strokeColor = tuple(d["strokeColor"]) hiddenColor = tuple(d["hiddenColor"]) showHidden = bool(d["showHidden"]) - focus = float(d["focus"]) if d.get("focus") else None + focus = float(d["focus"]) if d.get("focus") is not None else None - hlr = HLRBRep_Algo() - hlr.Add(shape.wrapped) - - coordinate_system = gp_Ax2(gp_Pnt(), gp_Dir(*projectionDir)) - - if focus is not None: - projector = HLRAlgo_Projector(coordinate_system, focus) - else: - projector = HLRAlgo_Projector(coordinate_system) - - hlr.Projector(projector) - hlr.Update() - hlr.Hide() - - hlr_shapes = HLRBRep_HLRToShape(hlr) - - visible = [] - - visible_sharp_edges = hlr_shapes.VCompound() - if not visible_sharp_edges.IsNull(): - visible.append(visible_sharp_edges) - - visible_smooth_edges = hlr_shapes.Rg1LineVCompound() - if not visible_smooth_edges.IsNull(): - visible.append(visible_smooth_edges) - - visible_contour_edges = hlr_shapes.OutLineVCompound() - if not visible_contour_edges.IsNull(): - visible.append(visible_contour_edges) - - hidden = [] - - hidden_sharp_edges = hlr_shapes.HCompound() - if not hidden_sharp_edges.IsNull(): - hidden.append(hidden_sharp_edges) - - hidden_contour_edges = hlr_shapes.OutLineHCompound() - if not hidden_contour_edges.IsNull(): - hidden.append(hidden_contour_edges) - - # Fix the underlying geometry - otherwise we will get segfaults - for el in visible: - BRepLib.BuildCurves3d_s(el, TOLERANCE) - for el in hidden: - BRepLib.BuildCurves3d_s(el, TOLERANCE) - - # convert to native CQ objects - visible = list(map(Shape, visible)) - hidden = list(map(Shape, hidden)) - (hiddenPaths, visiblePaths) = getPaths(visible, hidden) + visibleEdges, hiddenEdges = projectToViewpoint(shape, projectionDir, focus) + (hiddenPaths, visiblePaths) = getPaths(visibleEdges, hiddenEdges) # get bounding box -- these are all in 2D space - bb = Compound.makeCompound(hidden + visible).BoundingBox() + bb = Compound.makeCompound(hiddenEdges + visibleEdges).BoundingBox() # Determine whether the user wants to fit the drawing to the bounding box - if width == None or height == None: + if width is None or height is None: # Fit image to specified width (or height) - if width == None: + if width is None: width = (height - (2.0 * marginTop)) * ( bb.xlen / bb.ylen ) + 2.0 * marginLeft diff --git a/cadquery/occ_impl/projection.py b/cadquery/occ_impl/projection.py new file mode 100644 index 000000000..1aa12557e --- /dev/null +++ b/cadquery/occ_impl/projection.py @@ -0,0 +1,72 @@ +from typing import Optional + +from OCP.BRepLib import BRepLib +from OCP.gp import gp_Ax2, gp_Dir, gp_Pnt +from OCP.HLRAlgo import HLRAlgo_Projector +from OCP.HLRBRep import HLRBRep_Algo, HLRBRep_HLRToShape + +from .shapes import TOLERANCE, Edge, Shape + +DISCRETIZATION_TOLERANCE = 1e-3 + + +def projectToViewpoint( + shape, + projectionDir: tuple[float, float, float], + focus: Optional[float] = None, +) -> tuple[list[Edge], list[Edge]]: + hlr = HLRBRep_Algo() + hlr.Add(shape.wrapped) + + coordinate_system = gp_Ax2(gp_Pnt(), gp_Dir(*projectionDir)) + + if focus is not None: + projector = HLRAlgo_Projector(coordinate_system, focus) + else: + projector = HLRAlgo_Projector(coordinate_system) + + hlr.Projector(projector) + hlr.Update() + hlr.Hide() + + hlr_shapes = HLRBRep_HLRToShape(hlr) + + visible = [] + + visible_sharp_edges = hlr_shapes.VCompound() + if not visible_sharp_edges.IsNull(): + visible.append(visible_sharp_edges) + + visible_smooth_edges = hlr_shapes.Rg1LineVCompound() + if not visible_smooth_edges.IsNull(): + visible.append(visible_smooth_edges) + + visible_contour_edges = hlr_shapes.OutLineVCompound() + if not visible_contour_edges.IsNull(): + visible.append(visible_contour_edges) + + hidden = [] + + hidden_sharp_edges = hlr_shapes.HCompound() + if not hidden_sharp_edges.IsNull(): + hidden.append(hidden_sharp_edges) + + hidden_contour_edges = hlr_shapes.OutLineHCompound() + if not hidden_contour_edges.IsNull(): + hidden.append(hidden_contour_edges) + + # Fix the underlying geometry - otherwise we will get segfaults + for el in visible: + BRepLib.BuildCurves3d_s(el, TOLERANCE) + for el in hidden: + BRepLib.BuildCurves3d_s(el, TOLERANCE) + + # convert to native CQ objects + visible = [Shape.cast(s) for s in visible] # s is a TopoDS_Shape (Compound) + hidden = [Shape.cast(s) for s in hidden] + + # Extract edges + visible_edges = [e for c in visible for e in c.Edges()] + hidden_edges = [e for c in hidden for e in c.Edges()] + + return visible_edges, hidden_edges diff --git a/tests/test_projection.py b/tests/test_projection.py new file mode 100644 index 000000000..448e9816b --- /dev/null +++ b/tests/test_projection.py @@ -0,0 +1,61 @@ +import cadquery as cq +from cadquery.occ_impl.projection import projectToViewpoint +from cadquery.occ_impl.exporters.svg import exportSVG +from cadquery.occ_impl.shapes import Compound +from cadquery import Workplane + +viewpoint = { + "top": (0, 0, 1), + "left": (1, 0, 0), + "front": (0, 1, 0), + "ortho": (1, 1, 1), +} + + +def exportDXF3rdAngleProjection(my_part: Workplane, prefix: str) -> None: + for name, direction in viewpoint.items(): + visible_edges, hidden_edges = projectToViewpoint(my_part.val(), direction) + cq.exporters.exportDXF( + Compound.makeCompound(visible_edges), + f"{prefix}{name}.dxf", + doc_units=6, + ) + + +def exportSVG3rdAngleProjection(my_part, prefix: str) -> None: + for name, direction in viewpoint.items(): + exportSVG( + my_part, + f"{prefix}{name}.svg", + opts={ + "projectionDir": direction, + }, + ) + + +if __name__ == "__main__": + # Build the part + width = 10 + depth = 10 + height = 10 + + # !!! Test projection of fillets to arc segments in DXF. !!! + baseplate = ( + cq.Workplane("XY") # + .box(width, depth, height) + .edges("|Z") + .fillet(2.0) + ) + + hole_dia = 3.0 + + # !!! Test projection of countersunk to arc segments in DXF. !!! + drilled = ( + baseplate.faces(">Z") # + .workplane() + .cskHole(hole_dia, hole_dia * 2, 82.0) + ) + + # Expected DXF output to be identical to SVG output + exportSVG3rdAngleProjection(drilled, "") + exportDXF3rdAngleProjection(drilled, "")