Skip to content

Commit

Permalink
Text fixes and some shape/free func additions (#1700)
Browse files Browse the repository at this point in the history
* Fix regression [single letters]

* Text, offset, solid and exportBin changes

- text on path
- text on path/surface
- symmetric offset
- import/export binary brep

* Binary BREP in exporters/importers

* Extra tests

* Fix tests

* Add bin imp/exp test

* Blacken

* Remove one overload

* Fix dispatch

* Fix test

* Extra vis options

* Different error

* Fix/more tests

* Better coverage

* More tests

* Docs text placeholder

* Pseudo-infinite plane

* Add an example for text

* Black fix

* Apply suggestions from code review

Co-authored-by: Jeremy Wright <wrightjmf@gmail.com>

* Typo fixes

---------

Co-authored-by: Jeremy Wright <wrightjmf@gmail.com>
  • Loading branch information
adam-urbanczyk and jmwright authored Nov 24, 2024
1 parent a175cb8 commit 2629bf9
Show file tree
Hide file tree
Showing 10 changed files with 324 additions and 37 deletions.
6 changes: 5 additions & 1 deletion cadquery/occ_impl/exporters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@ class ExportTypes:
VTP = "VTP"
THREEMF = "3MF"
BREP = "BREP"
BIN = "BIN"


ExportLiterals = Literal[
"STL", "STEP", "AMF", "SVG", "TJS", "DXF", "VRML", "VTP", "3MF", "BREP"
"STL", "STEP", "AMF", "SVG", "TJS", "DXF", "VRML", "VTP", "3MF", "BREP", "BIN"
]


Expand Down Expand Up @@ -128,6 +129,9 @@ def export(
elif exportType == ExportTypes.BREP:
shape.exportBrep(fname)

elif exportType == ExportTypes.BIN:
shape.exportBin(fname)

else:
raise ValueError("Unknown export type")

Expand Down
17 changes: 16 additions & 1 deletion cadquery/occ_impl/importers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class ImportTypes:
STEP = "STEP"
DXF = "DXF"
BREP = "BREP"
BIN = "BIN"


class UNITS:
Expand All @@ -23,7 +24,7 @@ class UNITS:


def importShape(
importType: Literal["STEP", "DXF", "BREP"], fileName: str, *args, **kwargs
importType: Literal["STEP", "DXF", "BREP", "BIN"], fileName: str, *args, **kwargs
) -> "cq.Workplane":
"""
Imports a file based on the type (STEP, STL, etc)
Expand All @@ -39,6 +40,8 @@ def importShape(
return importDXF(fileName, *args, **kwargs)
elif importType == ImportTypes.BREP:
return importBrep(fileName)
elif importType == ImportTypes.BIN:
return importBin(fileName)
else:
raise RuntimeError("Unsupported import type: {!r}".format(importType))

Expand All @@ -60,6 +63,18 @@ def importBrep(fileName: str) -> "cq.Workplane":
return cq.Workplane("XY").newObject([shape])


def importBin(fileName: str) -> "cq.Workplane":
"""
Loads the binary BREP file as a single shape into a cadquery Workplane.
:param fileName: The path and name of the BREP file to be imported
"""
shape = Shape.importBin(fileName)

return cq.Workplane("XY").newObject([shape])


# Loads a STEP file into a CQ.Workplane object
def importStep(fileName: str) -> "cq.Workplane":
"""
Expand Down
187 changes: 161 additions & 26 deletions cadquery/occ_impl/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,8 @@

from OCP.LProp3d import LProp3d_CLProps

from OCP.BinTools import BinTools

from math import pi, sqrt, inf, radians, cos

import warnings
Expand Down Expand Up @@ -520,6 +522,26 @@ def importBrep(cls, f: Union[str, BytesIO]) -> "Shape":

return cls.cast(s)

def exportBin(self, f: Union[str, BytesIO]) -> bool:
"""
Export this shape to a binary BREP file.
"""

rv = BinTools.Write_s(self.wrapped, f)

return True if rv is None else rv

@classmethod
def importBin(cls, f: Union[str, BytesIO]) -> "Shape":
"""
Import shape from a binary BREP file.
"""
s = TopoDS_Shape()

BinTools.Read_s(s, f)

return cls.cast(s)

def geomType(self) -> Geoms:
"""
Gets the underlying geometry type.
Expand Down Expand Up @@ -4528,6 +4550,7 @@ def _normalize(s: Shape) -> Shape:
"""
Apply some normalizations:
- Shell with only one Face -> Face.
- Compound with only one element -> element.
"""

t = s.ShapeType()
Expand Down Expand Up @@ -4724,17 +4747,26 @@ def shell(s: Sequence[Shape], tol: float = 1e-6) -> Shape:


@multimethod
def solid(*s: Shape, tol: float = 1e-6) -> Shape:
def solid(s1: Shape, *sn: Shape, tol: float = 1e-6) -> Shape:
"""
Build solid from faces.
Build solid from faces or shells.
"""

builder = ShapeFix_Solid()

faces = [f for el in s for f in _get(el, "Face")]
rv = builder.SolidFromShell(shell(*faces, tol=tol).wrapped)
# get both Shells and Faces
s = [s1, *sn]
shells_faces = [f for el in s for f in _get(el, ("Shell", "Face"))]

return _compound_or_shape(rv)
# if no shells are present, use faces to construct them
shells = [el.wrapped for el in shells_faces if el.ShapeType() == "Shell"]
if not shells:
faces = [el for el in shells_faces]
shells = [shell(*faces, tol=tol).wrapped]

rvs = [builder.SolidFromShell(sh) for sh in shells]

return _compound_or_shape(rvs)


@solid.register
Expand Down Expand Up @@ -4922,9 +4954,10 @@ def ellipse(r1: float, r2: float) -> Shape:
)


def plane(w: float, l: float) -> Shape:
@multimethod
def plane(w: Real, l: Real) -> Shape:
"""
Construct a planar face.
Construct a finite planar face.
"""

pln_geom = gp_Pln(Vector(0, 0, 0).toPnt(), Vector(0, 0, 1).toDir())
Expand All @@ -4934,6 +4967,24 @@ def plane(w: float, l: float) -> Shape:
)


@plane.register
def plane() -> Shape:
"""
Construct an infinite planar face.
This is a crude approximation. Truly infinite faces in OCCT do not work as
expected in all contexts.
"""

INF = 1e60

pln_geom = gp_Pln(Vector(0, 0, 0).toPnt(), Vector(0, 0, 1).toDir())

return _compound_or_shape(
BRepBuilderAPI_MakeFace(pln_geom, -INF, INF, -INF, INF).Face()
)


def box(w: float, l: float, h: float) -> Shape:
"""
Construct a solid box.
Expand Down Expand Up @@ -5012,9 +5063,10 @@ def cone(d: Real, h: Real) -> Shape:
return cone(d, 0, h)


@multimethod
def text(
txt: str,
size: float,
size: Real,
font: str = "Arial",
path: Optional[str] = None,
kind: Literal["regular", "bold", "italic"] = "regular",
Expand Down Expand Up @@ -5065,7 +5117,67 @@ def text(
font_i, NCollection_Utf8String(txt), theHAlign=theHAlign, theVAlign=theVAlign
)

return clean(_compound_or_shape(rv).faces().fuse())
return clean(compound(_compound_or_shape(rv).faces()).fuse())


@text.register
def text(
txt: str,
size: Real,
spine: Shape,
planar: bool = False,
font: str = "Arial",
path: Optional[str] = None,
kind: Literal["regular", "bold", "italic"] = "regular",
halign: Literal["center", "left", "right"] = "center",
valign: Literal["center", "top", "bottom"] = "center",
) -> Shape:
"""
Create a text on a spine.
"""

spine = _get_one_wire(spine)
L = spine.Length()

rv = []
for el in text(txt, size, font, path, kind, halign, valign):
pos = el.BoundingBox().center.x

# position
rv.append(
el.moved(-pos)
.moved(rx=-90 if planar else 0, ry=-90)
.moved(spine.locationAt(pos / L))
)

return _normalize(compound(rv))


@text.register
def text(
txt: str,
size: Real,
spine: Shape,
base: Shape,
font: str = "Arial",
path: Optional[str] = None,
kind: Literal["regular", "bold", "italic"] = "regular",
halign: Literal["center", "left", "right"] = "center",
valign: Literal["center", "top", "bottom"] = "center",
) -> Shape:
"""
Create a text on a spine and a base surface.
"""

base = _get_one(base, "Face")

tmp = text(txt, size, spine, False, font, path, kind, halign, valign)

rv = []
for f in tmp.faces():
rv.append(f.project(base, f.normalAt()))

return _normalize(compound(rv))


#%% ops
Expand Down Expand Up @@ -5266,33 +5378,56 @@ def revolve(s: Shape, p: VectorLike, d: VectorLike, a: float = 360):
return _compound_or_shape(results)


def offset(s: Shape, t: float, cap=True, tol: float = 1e-6) -> Shape:
def offset(
s: Shape, t: float, cap=True, both: bool = False, tol: float = 1e-6
) -> Shape:
"""
Offset or thicken faces or shells.
"""

builder = BRepOffset_MakeOffset()
def _offset(t):

results = []
results = []

for el in _get(s, ("Face", "Shell")):
for el in _get(s, ("Face", "Shell")):

builder.Initialize(
el.wrapped,
t,
tol,
BRepOffset_Mode.BRepOffset_Skin,
False,
False,
GeomAbs_Intersection,
cap,
)
builder = BRepOffset_MakeOffset()

builder.MakeOffsetShape()
builder.Initialize(
el.wrapped,
t,
tol,
BRepOffset_Mode.BRepOffset_Skin,
False,
False,
GeomAbs_Intersection,
cap,
)

results.append(builder.Shape())
builder.MakeOffsetShape()

return _compound_or_shape(results)
results.append(builder.Shape())

return results

if both:
results_pos = _offset(t)
results_neg = _offset(-t)

results_both = [
Shape(el1) + Shape(el2) for el1, el2 in zip(results_pos, results_neg)
]

if len(results_both) == 1:
rv = results_both[0]
else:
rv = Compound.makeCompound(results_both)

else:
results = _offset(t)
rv = _compound_or_shape(results)

return rv


@multimethod
Expand Down
17 changes: 15 additions & 2 deletions cadquery/vis.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,14 @@ def _to_vtk_axs(locs: List[Location], scale: float = 0.1) -> vtkActor:
return rv


def show(*objs: Showable, scale: float = 0.2, alpha: float = 1, **kwrags: Any):
def show(
*objs: Showable,
scale: float = 0.2,
alpha: float = 1,
tolerance: float = 1e-3,
edges: bool = False,
**kwrags: Any,
):
"""
Show CQ objects using VTK.
"""
Expand All @@ -145,10 +152,16 @@ def show(*objs: Showable, scale: float = 0.2, alpha: float = 1, **kwrags: Any):
axs = _to_vtk_axs(locs, scale=scale)

# create a VTK window
win = _vtkRenderWindow(assy)
win = _vtkRenderWindow(assy, tolerance=tolerance)

win.SetWindowName("CQ viewer")

# get renderer and actor
if edges:
ren = win.GetRenderers().GetFirstRenderer()
for act in ren.GetActors():
act.GetProperty().EdgeVisibilityOn()

# rendering related settings
win.SetMultiSamples(16)
vtkMapper.SetResolveCoincidentTopologyToPolygonOffset()
Expand Down
Loading

0 comments on commit 2629bf9

Please sign in to comment.