diff --git a/cadquery/func.py b/cadquery/func.py index 949bc809b..b2c86bfe0 100644 --- a/cadquery/func.py +++ b/cadquery/func.py @@ -59,6 +59,7 @@ chamfer2D, draft, History, + hlr, ) __all__ = [ @@ -124,4 +125,5 @@ "fillet2D", "draft", "History", + "hlr", ] diff --git a/cadquery/occ_impl/exporters/__init__.py b/cadquery/occ_impl/exporters/__init__.py index d27bc7a57..266dd4ac9 100644 --- a/cadquery/occ_impl/exporters/__init__.py +++ b/cadquery/occ_impl/exporters/__init__.py @@ -1,21 +1,16 @@ -import tempfile -import os -import io as StringIO - from typing import IO, Optional, Union, cast, Dict, Any, Iterable from typing_extensions import Literal from OCP.VrmlAPI import VrmlAPI -from ...utils import deprecate from ..shapes import Shape, compound from ...types import UnitLiterals -from .svg import getSVG +from .svg import getSVG, exportSVG from .json import JsonMesh from .amf import AmfWriter from .threemf import ThreeMFWriter -from .dxf import exportDXF, DxfDocument +from .dxf import exportDXF, exportDXFProjection, DxfDocument from .vtk import exportVTP diff --git a/cadquery/occ_impl/exporters/dxf.py b/cadquery/occ_impl/exporters/dxf.py index 41ae15224..0cc69f78b 100644 --- a/cadquery/occ_impl/exporters/dxf.py +++ b/cadquery/occ_impl/exporters/dxf.py @@ -22,8 +22,8 @@ from typing_extensions import Self from ...units import RAD2DEG -from ..shapes import Face, Edge, Shape, Compound, compound -from ..geom import Plane +from ..shapes import Face, Edge, Shape, Compound, compound, hlr +from ..geom import Plane, VectorLike ApproxOptions = Literal["spline", "arc"] @@ -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 = {} @@ -365,7 +366,7 @@ def _dxf_spline(cls, edge: Edge, plane: Plane) -> DxfEntityAttributes: def exportDXF( - w: Union[WorkplaneLike, Shape, Iterable[Shape]], + w: WorkplaneLike | Shape | Iterable[Shape], fname: str, approx: Optional[ApproxOptions] = None, tolerance: float = 1e-3, @@ -394,3 +395,38 @@ def exportDXF( zoom.extents(dxf.msp) dxf.document.saveas(fname) + + +def exportDXFProjection( + w: Union[WorkplaneLike, Shape], + pnt: VectorLike, + dir: VectorLike, + fname: str, + approx: Optional[ApproxOptions] = None, + tolerance: float = 1e-3, + *, + doc_units: int = units.MM, +) -> None: + """ + Export Workplane or shape content to DXF using projections. Works with 3D objects. + + :param w: Workplane to be exported. + :param pnt: Center of projection. + :param dir: Direction of projection. + :param fname: Output filename. + :param approx: Approximation strategy. None means no approximation is applied. + "spline" results in all splines being approximated as cubic splines. "arc" results + in all curves being approximated as arcs and straight segments. + :param tolerance: Approximation tolerance. + :param doc_units: ezdxf document/modelspace :doc:`units ` (in. = ``1``, mm = ``4``). + """ + + shapes = [] + + if isinstance(w, WorkplaneLike): + for s in w.__iter__(): + shapes.append(hlr(s, pnt, dir)[0]) + else: + shapes.append(hlr(w, pnt, dir)[0]) + + exportDXF(shapes, fname, approx, tolerance, doc_units=doc_units) diff --git a/cadquery/occ_impl/exporters/svg.py b/cadquery/occ_impl/exporters/svg.py index b41dee7b0..b742a6b87 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 ..shapes import hlr -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,9 @@ 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,13 +113,11 @@ 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) @@ -186,64 +183,16 @@ def getSVG(shape, opts=None): showHidden = bool(d["showHidden"]) focus = float(d["focus"]) if d.get("focus") 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 = hlr(shape, (0, 0, 0), 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/shapes.py b/cadquery/occ_impl/shapes.py index a95021c65..0e3a396bf 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -159,6 +159,9 @@ BRepAlgoAPI_Check, ) +from OCP.HLRAlgo import HLRAlgo_Projector +from OCP.HLRBRep import HLRBRep_Algo, HLRBRep_HLRToShape + from OCP.Geom import ( Geom_BezierCurve, Geom_ConicalSurface, @@ -7971,3 +7974,63 @@ def closest(s1: Shape, s2: Shape) -> tuple[Vector, Vector]: assert ext.Perform() return Vector(ext.PointOnShape1(1)), Vector(ext.PointOnShape2(1)) + + +def hlr( + shape, pnt: VectorLike, dir: VectorLike, focus: float | None = None, +) -> tuple[Compound, Compound]: + """ + Project shape onto a plane defined by pnt and dir and apply hidden line removal. Useful for + generating views for technical drawings. + """ + hlr = HLRBRep_Algo() + hlr.Add(shape.wrapped) + + coordinate_system = gp_Ax2(Vector(pnt).toPnt(), Vector(dir).toDir()) + + 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) + + # Extract edges + visible_edges = compound(_compound_or_shape(visible).Edges()) + hidden_edges = compound(_compound_or_shape(hidden).Edges()) + + return visible_edges, hidden_edges diff --git a/examples/Ex102_DXF_with_HLR.py b/examples/Ex102_DXF_with_HLR.py new file mode 100644 index 000000000..ee893f213 --- /dev/null +++ b/examples/Ex102_DXF_with_HLR.py @@ -0,0 +1,37 @@ +from cadquery import Workplane, exporters + +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(): + exporters.exportDXFProjection( + my_part, (0, 0, 0), direction, f"{prefix}{name}.dxf", doc_units=6, + ) + + +def exportSVG3rdAngleProjection(my_part, prefix: str) -> None: + for name, direction in viewpoint.items(): + exporters.exportSVG( + my_part, f"{prefix}{name}.svg", opts={"projectionDir": direction,}, + ) + + +# Build the part +width = 10 +depth = 10 +height = 10 +hole_dia = 3.0 + +baseplate = Workplane("XY").box(width, depth, height).edges("|Z").fillet(2.0) # +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, "") +exportDXF3rdAngleProjection(drilled.val(), "shape_")