gumyr / cq_warehouse

A cadquery parametric part collection
Apache License 2.0
107 stars 23 forks source link

Make cq_object optional via sub-classing #63

Closed gumyr closed 1 year ago

gumyr commented 1 year ago

By sub-classing all of the objects created with cq_warehouse the use of .cq_object will become unnecessary and result in a more natural user interface. For example:

flanged_nut = HexNutWithFlange(size="M6-1", fastener_type="din1665")
...
show_object(flanged_nut)

will be possible.

The changes to do this are as follows:

  1. The class definition must specify the super-class: e.g. class Nut(ABC,cq.Compound):. Note that the Nut body is of type Solid but once the thread is added it becomes type Compound therefore the base class must be determined for all objects.
  2. The __init__ method must initialize the super class as follows: super().__init__(self._cq_object.wrapped)
  3. A deprecation warning needs to be placed with all the cq_object property definitions. e.g. warn("cq_object will be deprecated.", DeprecationWarning, stacklevel=2)
  4. The appropriate documentation needs to be changed such that use of cq_object is deprecated.

Note that for derived classes, only the base class need to change.

Thread will require more change as currently the actual thread object isn't created until the cq_object property is accessed - for performance reasons when building threaded objects with the simple option. A new optional parameter simple may need to be introduced to thread which will create a null thread object.

gumyr commented 1 year ago

This doesn't look to be feasible after all. Consider this example:

from cadquery import Solid, Vector
from cadquery.occ_impl.shapes import VectorLike

class ChamferBox(Solid):
    def __init__(
        self,
        length: float,
        width: float,
        height: float,
        chamfer_size: float,
        pnt: VectorLike = Vector(0, 0, 0),
        dir: VectorLike = Vector(0, 0, 1),
    ) -> Solid:
        box = Solid.makeBox(length, width, height, pnt, dir)
        box = box.chamfer(chamfer_size, None, box.Edges())
        super().__init__(box.wrapped)

chamfer_box = ChamferBox(1, 1, 1, 0.1)
if "show_object" in locals():
    show_object(chamfer_box)

print(
    f"{type(chamfer_box)=},{isinstance(chamfer_box, Solid)=},{chamfer_box.isValid()=}"
)

which generates: image

type(chamfer_box)=<class '__main__.ChamferBox'>,isinstance(chamfer_box, Solid)=True,chamfer_box.isValid()=True

However, if a one does translated_chamfer_box = chamfer_box.translate((1, 0, 0)) the following traceback is generated:

Traceback (most recent call last):
  File "/home/roger/Documents/Bugs/subclass.py", line 27, in <module>
    translated_chamfer_box = chamfer_box.translate((1, 0, 0))
  File "/home/roger/anaconda3/envs/cqJuly/lib/python3.9/site-packages/cadquery/occ_impl/shapes.py", line 892, in translate
    return self._apply_transform(T)
  File "/home/roger/anaconda3/envs/cqJuly/lib/python3.9/site-packages/cadquery/occ_impl/shapes.py", line 852, in _apply_transform
    return self.__class__(BRepBuilderAPI_Transform(self.wrapped, Tr, True).Shape())
TypeError: __init__() missing 3 required positional arguments: 'width', 'height', and 'chamfer_size'

The problem seems to be how _apply_transform() (and many other methods) is/are implemented - it/they take apart the object and rebuild it with the object's Shape() which works for the base classes but with a sub-classed object Shape() doesn't provide sufficient information to recreate the object.

    def _apply_transform(self: T, Tr: gp_Trsf) -> T:

        return self.__class__(BRepBuilderAPI_Transform(self.wrapped, Tr, True).Shape())

In addition, there appears to be no way to subclass an Assembly.

It seems as though significant development in the cadquery direct API layer would need to be done to support sub-classing.