From 9994a5828b846cb0649a391d0a853e29f8208831 Mon Sep 17 00:00:00 2001 From: fpq473 Date: Thu, 1 Sep 2022 20:40:11 -0700 Subject: [PATCH] FilletBox as a solid-like object instead of a Solid subclass See examples/fillet-box.py for FilletBox following a "SolidLike" protocol in order to be treated like a solid by Workplane. See cadquery/cq.py for an example of the kind of change that Workplane will need to support SolidLike. Currently only Workplane.union() has been adapted. This is the Workplane method demonstrated in examples/fillet-box.py. --- cadquery/cq.py | 5 ++- cadquery/occ_impl/shapes.py | 23 +++++++++++ examples/fillet-box.py | 79 +++++++++++++++++++++++++++++++++++++ examples/locatable.py | 58 +++++++++++++++++++++++++++ 4 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 examples/fillet-box.py create mode 100644 examples/locatable.py diff --git a/cadquery/cq.py b/cadquery/cq.py index 9cde44489..c49791c98 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -45,6 +45,7 @@ Wire, Face, Solid, + SolidLike, Compound, wiresToFaces, ) @@ -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, @@ -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))) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 639a941ef..e7c7603af 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -11,6 +11,7 @@ overload, TypeVar, cast as tcast, + runtime_checkable, ) from typing_extensions import Literal, Protocol @@ -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( @@ -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: + ... diff --git a/examples/fillet-box.py b/examples/fillet-box.py new file mode 100644 index 000000000..831dcc866 --- /dev/null +++ b/examples/fillet-box.py @@ -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)=}") diff --git a/examples/locatable.py b/examples/locatable.py new file mode 100644 index 000000000..37f7de713 --- /dev/null +++ b/examples/locatable.py @@ -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)