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
5 changes: 4 additions & 1 deletion cadquery/cq.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
Wire,
Face,
Solid,
SolidLike,
Compound,
wiresToFaces,
)
Expand Down Expand Up @@ -3275,7 +3276,7 @@ def combine(

def union(
self: T,
toUnion: Optional[Union["Workplane", Solid, Compound]] = None,
toUnion: Optional[Union["Workplane", SolidLike, Compound]] = None,
clean: bool = True,
glue: bool = False,
tol: Optional[float] = None,
Expand Down Expand Up @@ -3304,6 +3305,8 @@ def union(
self._mergeTags(toUnion)
elif isinstance(toUnion, (Solid, Compound)):
newS = [toUnion]
elif isinstance(toUnion, SolidLike):
newS = [Solid(toUnion)]
else:
raise ValueError("Cannot union type '{}'".format(type(toUnion)))

Expand Down
23 changes: 23 additions & 0 deletions cadquery/occ_impl/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
overload,
TypeVar,
cast as tcast,
runtime_checkable,
)
from typing_extensions import Literal, Protocol

Expand Down Expand Up @@ -2779,6 +2780,22 @@ class Solid(Shape, Mixin3D):

wrapped: TopoDS_Solid

@overload
def __init__(self, obj: "SolidLike"):
...

@overload
def __init__(self, obj: TopoDS_Shape):
...

def __init__(self, obj):
if isinstance(obj, SolidLike):
obj = obj.__cadquery_solid__().wrapped
super().__init__(obj)

def __cadquery_solid__(self: T) -> T:
return self

@classmethod
@deprecate()
def interpPlate(
Expand Down Expand Up @@ -3642,3 +3659,9 @@ def edgesToWires(edges: Iterable[Edge], tol: float = 1e-6) -> List[Wire]:
ShapeAnalysis_FreeBounds.ConnectEdgesToWires_s(edges_in, tol, False, wires_out)

return [Wire(el) for el in wires_out]


@runtime_checkable
class SolidLike(Protocol):
def __cadquery_solid__(self) -> Solid:
...
79 changes: 79 additions & 0 deletions examples/fillet-box.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from OCP.gp import gp_Trsf, gp_XYZ
from cadquery import Solid, Workplane
from locatable import LocatableMixin


class FilletBox(LocatableMixin):
def __init__(
self,
length: float,
width: float,
height: float,
radius: float,
xform: gp_Trsf = gp_Trsf(),
):
self.length = length
self.width = width
self.height = height
self.radius = radius
# Don't have to use gp_Trsf for transforms, but using it here
# as it's conveniently available and I sort of understand it.
self.xform = xform

def __cadquery_solid__(self) -> Solid:
obj = Solid.makeBox(self.length, self.width, self.height)
obj_edges = obj.Edges()
fobj: Solid = obj.fillet(self.radius, obj_edges)
# XXX - Use of private Solid._apply_transform() but can
# avoided easily by starting with self.xform and calling the
# Solid.scale(), Solid.rotate(), and Solid.translate(). Or
# Solid could have a public way of accepting a gp_Trsf.
return fobj._apply_transform(self.xform)

def _apply_transform(self, Tr: gp_Trsf) -> "FilletBox":
return FilletBox(
self.length,
self.width,
self.height,
self.radius,
self.xform * Tr,
)

def scale(self, factor: float) -> "FilletBox":
"""
Scale this shape, but only if it hasn't been translated

Suppose you wanted an object that cannot be scaled (for
example, maybe it doesn't make sense to scale an M6 nut).
This is where you would forbid the user.

Here I've done something sillier which is to forbid scaling if
the object has been translated. Just a demonstration of where
customizations would go.

"""
trans: gp_XYZ = self.xform.TranslationPart()
if (trans.X(), trans.Y(), trans.Z()) != (0, 0, 0):
raise Exception("cannot scale a translated object")
return FilletBox(
self.length * factor,
self.width * factor,
self.height * factor,
self.radius * factor,
self.xform,
)


fillet_box = FilletBox(length=10, width=8, height=5, radius=1)

print(f"{fillet_box=}")
print(f"{fillet_box.translate((1, 2, 3))=}")
print(f"{fillet_box.scale(2)=}")
print(f"{Solid(fillet_box)=}")
print(f"{Workplane().union(fillet_box).findSolid()=}")


from cadquery.cq import SolidLike # not usually needed

print(f"{isinstance(fillet_box, SolidLike)=}")
print(f"{isinstance(Solid.makeBox(3,4,5), SolidLike)=}")
58 changes: 58 additions & 0 deletions examples/locatable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from OCP.gp import gp_Pnt, gp_Trsf
from cadquery import Vector
from cadquery.occ_impl.geom import VectorLike
from typing import Protocol, TypeVar


T = TypeVar("T")
TLocatable = TypeVar("TLocatable", bound="Locatable")


class Locatable(Protocol):

"""A class that implements this protocol (i.e. implements the below
method) can be used with LocatableMixin

"""

def _apply_transform(self: TLocatable, Tr: gp_Trsf) -> TLocatable:
"""
Make a copy of the current object with `Tr` applied

"""
...


class LocatableMixin:

"""
Mixin for adding methods for changing location

These are currently just copied from Solid, but they could (or
should?) written in be written some OCP-independent way.

Solid-like objects that want to be locatable can inherit this
mixin and implement `_apply_transform()`. Or they are free to
implement any rotate, translate, and scale methods they want.

"""

def translate(self: TLocatable, vector: VectorLike) -> TLocatable:
"""
Translates this shape through a transformation.
"""

T = gp_Trsf()
T.SetTranslation(Vector(vector).wrapped)

return self._apply_transform(T)

def scale(self: TLocatable, factor: float) -> TLocatable:
"""
Scales this shape through a transformation.
"""

T = gp_Trsf()
T.SetScale(gp_Pnt(), factor)

return self._apply_transform(T)