Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cadquery/func.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@
chamfer2D,
draft,
History,
HLRResult,
hlr,
)

__all__ = [
Expand Down Expand Up @@ -124,4 +126,6 @@
"fillet2D",
"draft",
"History",
"HLRResult",
"hlr",
]
9 changes: 2 additions & 7 deletions cadquery/occ_impl/exporters/__init__.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down
46 changes: 43 additions & 3 deletions cadquery/occ_impl/exporters/dxf.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""DXF export utilities."""

from os import PathLike
from typing import (
Any,
Dict,
Expand All @@ -22,8 +23,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"]
Expand Down Expand Up @@ -157,6 +158,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 = {}
Expand Down Expand Up @@ -365,7 +367,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,
Expand Down Expand Up @@ -394,3 +396,41 @@ def exportDXF(

zoom.extents(dxf.msp)
dxf.document.saveas(fname)


def exportDXFProjection(
s: Union[WorkplaneLike, Shape],
path: PathLike | str,
dir: VectorLike,
pnt: VectorLike = (0, 0, 0),
approx: Optional[ApproxOptions] = None,
tolerance: float = 1e-3,
*,
up: Optional[VectorLike] = None,
doc_units: int = units.MM,
) -> None:
"""
Export to DXF using projections. Works with 3D objects.

:param s: Shape or Workplane to be exported.
:param path: Output file path.
:param dir: Direction of projection.
:param pnt: Origin of the projection plane.
:param up: Direction that should appear upward in the projected output. None
preserves OCCT's default in-plane orientation.
: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 <ezdxf-stable:concepts/units>` (in. = ``1``, mm = ``4``).
"""

shapes = []

if isinstance(s, WorkplaneLike):
for s in s.__iter__():
shapes.append(hlr(s, dir, pnt, up=up).visible)
else:
shapes.append(hlr(s, dir, pnt, up=up).visible)

exportDXF(shapes, str(path), approx, tolerance, doc_units=doc_units)
89 changes: 22 additions & 67 deletions cadquery/occ_impl/exporters/svg.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import io as StringIO

from ..shapes import Shape, Compound, TOLERANCE
from ..shapes import 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
Expand Down Expand Up @@ -106,21 +103,21 @@ 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.
"""

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)

Expand All @@ -138,8 +135,10 @@ def getSVG(shape, opts=None):
marginLeft: Inset margin from the left side of the document.
marginTop: Inset margin from the top side of the document.
projectionDir: Direction the camera will view the shape from.
up: Direction that should appear upward in the projected output. None
preserves OCCT's default in-plane orientation.
showAxes: Whether or not to show the axes indicator, which will only be
visible when the projectionDir is also at the default.
visible when projectionDir and up are also at the defaults.
strokeWidth: Width of the line that visible edges are drawn with.
strokeColor: Color of the line that visible edges are drawn with.
hiddenColor: Color of the line that hidden edges are drawn with.
Expand All @@ -155,6 +154,7 @@ def getSVG(shape, opts=None):
"marginLeft": 200,
"marginTop": 20,
"projectionDir": (-1.75, 1.1, 5),
"up": None,
"showAxes": True,
"strokeWidth": -1.0, # -1 = calculated based on unitScale
"strokeColor": (0, 0, 0), # RGB 0-255
Expand All @@ -179,71 +179,26 @@ def getSVG(shape, opts=None):
marginLeft = float(d["marginLeft"])
marginTop = float(d["marginTop"])
projectionDir = tuple(d["projectionDir"])
up = d["up"]
showAxes = bool(d["showAxes"])
strokeWidth = float(d["strokeWidth"])
strokeColor = tuple(d["strokeColor"])
hiddenColor = tuple(d["hiddenColor"])
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)
hlr_result = hlr(shape, projectionDir, up=up, focus=focus)
visibleEdges = hlr_result.visible
hiddenEdges = hlr_result.hidden
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
Comment thread
adam-urbanczyk marked this conversation as resolved.
Expand Down Expand Up @@ -280,7 +235,7 @@ def getSVG(shape, opts=None):
visibleContent += PATHTEMPLATE % p

# If the caller wants the axes indicator and is using the default direction, add in the indicator
if showAxes and projectionDir == (-1.75, 1.1, 5):
if showAxes and projectionDir == (-1.75, 1.1, 5) and up is None:
axesIndicator = AXES_TEMPLATE % (
{"unitScale": str(unitScale), "textboxY": str(height - 30), "uom": str(uom)}
)
Expand Down
Loading
Loading