The RawShape and RawPart declarations let you create a shape or shapes using python and OCCT bindings directly. Once created they can be used within DeclaraCAD as any other shape.

To use RawShape, create a subclass of it and implement the get_shape method. It should return a TopoDS_Shape instance.

The python-occ sprocket example demonstrates this:

"""
This example demonstrates how to use RawShape to build a shape using OCCT
directly and include it with other DeclaraCAD shapes.

Copyright 2016 Luke Chen (@chentao807, https://github.com/chentao807)

This file is part of pythonOCC.

pythonOCC is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

pythonOCC is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with pythonOCC.  If not, see <http://www.gnu.org/licenses/

This example is a port of the C++ code
available at http://www.algotopia.com/contents/opencascade/opencascade_sprocket
"""
from declaracad.occ.api import Part, RawShape, Transform

import sys
from math import pi as M_PI, sin, cos, pow, atan

from OCCT.gp import (
    gp, gp_Pnt2d, gp_Ax2d, gp_Dir2d, gp_Circ2d, gp_Ax2, gp_Lin2d, gp_Trsf,
    gp_Pnt, gp_Vec, gp_Ax3, gp_Pln
)

from OCCT.GCE2d import GCE2d_MakeArcOfCircle, GCE2d_MakeCircle, GCE2d_MakeLine
from OCCT.Geom2dAPI import Geom2dAPI_InterCurveCurve
from OCCT.GeomAPI import GeomAPI
from OCCT.BRepBuilderAPI import (BRepBuilderAPI_MakeEdge,
                                BRepBuilderAPI_MakeWire,
                                BRepBuilderAPI_MakeFace,
                                BRepBuilderAPI_Transform)
from OCCT.BRepPrimAPI import (BRepPrimAPI_MakePrism, BRepPrimAPI_MakeRevol,
                             BRepPrimAPI_MakeCylinder, BRepPrimAPI_MakeCone)
from OCCT.GccAna import GccAna_Circ2d2TanRad
from OCCT.BRepAlgoAPI import BRepAlgoAPI_Cut, BRepAlgoAPI_Fuse
from OCCT.BRepFilletAPI import BRepFilletAPI_MakeFillet2d
from OCCT.BRepTools import BRepTools_WireExplorer


roller_diameter = 10.2
pitch = 15.875
num_teeth = 40
chain_width = 6.35

#  Dimensions derived from the provided inputs
roller_radius = roller_diameter / 2.
tooth_angle = (2 * M_PI) / num_teeth
pitch_circle_diameter = pitch / sin(tooth_angle / 2.)
pitch_circle_radius = pitch_circle_diameter / 2.

roller_contact_angle_min = (M_PI * 120 / 180) - ((M_PI / 2.) / num_teeth)
roller_contact_angle_max = (M_PI * 140 / 180) - ((M_PI / 2.) / num_teeth)
roller_contact_angle = (roller_contact_angle_min + roller_contact_angle_max) / 2.

tooth_radius_min = 0.505 * roller_diameter
tooth_radius_max = tooth_radius_min + (0.069 * pow(roller_diameter, 1.0 / 3.0))
tooth_radius = (tooth_radius_min + tooth_radius_max) / 2.

profile_radius = 0.12 * roller_diameter * (num_teeth + 2)
top_diameter = pitch_circle_diameter + ((1 - (1.6 / num_teeth)) * pitch) - roller_diameter
top_radius = top_diameter / 2.

thickness = chain_width * 0.95

# Center hole data
center_radius = 125.0 / 2.

# Mounting hole data
mounting_hole_count = 6
mounting_radius = 153.0 / 2.
hole_radius = 8.5 / 2.


def build_tooth():
    base_center = gp_Pnt2d(pitch_circle_radius + (tooth_radius - roller_radius), 0)
    base_circle = gp_Circ2d(gp_Ax2d(base_center, gp_Dir2d()), tooth_radius)
    trimmed_base = GCE2d_MakeArcOfCircle(base_circle,
                                         M_PI - (roller_contact_angle / 2.),
                                         M_PI).Value()
    trimmed_base.Reverse()  # just a trick
    p0 = trimmed_base.StartPoint()
    p1 = trimmed_base.EndPoint()

    # Determine the center of the profile circle
    x_distance = cos(roller_contact_angle / 2.) * (profile_radius + tooth_radius)
    y_distance = sin(roller_contact_angle / 2.) * (profile_radius + tooth_radius)
    profile_center = gp_Pnt2d(pitch_circle_radius - x_distance, y_distance)

    # Construct the profile circle gp_Circ2d
    profile_circle = gp_Circ2d(gp_Ax2d(profile_center, gp_Dir2d()),
                               profile_center.Distance(p1))
    geom_profile_circle = GCE2d_MakeCircle(profile_circle).Value()

    # Construct the outer circle gp_Circ2d
    outer_circle = gp_Circ2d(gp_Ax2d(gp_Pnt2d(0, 0), gp_Dir2d()), top_radius)
    geom_outer_circle = GCE2d_MakeCircle(outer_circle).Value()

    inter = Geom2dAPI_InterCurveCurve(geom_profile_circle, geom_outer_circle)
    num_points = inter.NbPoints()
    assert isinstance(p1, gp_Pnt2d)
    if num_points == 2:
        if p1.Distance(inter.Point(1)) < p1.Distance(inter.Point(2)):
            p2 = inter.Point(1)
        else:
            p2 = inter.Point(2)
    elif num_points == 1:
        p2 = inter.Point(1)
    else:
        sys.exit(-1)

    # Trim the profile circle and mirror
    trimmed_profile = GCE2d_MakeArcOfCircle(profile_circle, p1, p2).Value()

    # Calculate the outermost point
    p3 = gp_Pnt2d(cos(tooth_angle / 2.) * top_radius,
                  sin(tooth_angle / 2.) * top_radius)

    # and use it to create the third arc
    trimmed_outer = GCE2d_MakeArcOfCircle(outer_circle, p2, p3).Value()

    # Mirror and reverse the three arcs
    mirror_axis = gp_Ax2d(gp.Origin2d_(), gp.DX2d_().Rotated(tooth_angle / 2.))

    mirror_base = trimmed_base.Copy()
    mirror_profile = trimmed_profile.Copy()
    mirror_outer = trimmed_outer.Copy()

    mirror_base.Mirror(mirror_axis)
    mirror_profile.Mirror(mirror_axis)
    mirror_outer.Mirror(mirror_axis)

    mirror_base.Reverse()
    mirror_profile.Reverse()
    mirror_outer.Reverse()

    # Replace the two outer arcs with a single one
    outer_start = trimmed_outer.StartPoint()
    outer_mid = trimmed_outer.EndPoint()
    outer_end = mirror_outer.EndPoint()

    outer_arc = GCE2d_MakeArcOfCircle(outer_start, outer_mid, outer_end).Value()

    # Create an arc for the inside of the wedge
    inner_circle = gp_Circ2d(gp_Ax2d(gp_Pnt2d(0, 0), gp_Dir2d()),
                             top_radius - roller_diameter)
    inner_start = gp_Pnt2d(top_radius - roller_diameter, 0)
    inner_arc = GCE2d_MakeArcOfCircle(inner_circle, inner_start, tooth_angle).Value()
    inner_arc.Reverse()

    # Convert the 2D arcs and two extra lines to 3D edges
    plane = gp_Pln(gp.Origin_(), gp.DZ_())
    arc1 = BRepBuilderAPI_MakeEdge(GeomAPI.To3d_(trimmed_base, plane)).Edge()
    arc2 = BRepBuilderAPI_MakeEdge(GeomAPI.To3d_(trimmed_profile, plane)).Edge()
    arc3 = BRepBuilderAPI_MakeEdge(GeomAPI.To3d_(outer_arc, plane)).Edge()
    arc4 = BRepBuilderAPI_MakeEdge(GeomAPI.To3d_(mirror_profile, plane)).Edge()
    arc5 = BRepBuilderAPI_MakeEdge(GeomAPI.To3d_(mirror_base, plane)).Edge()

    p4 = mirror_base.EndPoint()
    p5 = inner_arc.StartPoint()

    lin1 = BRepBuilderAPI_MakeEdge(gp_Pnt(p4.X(), p4.Y(), 0),
                                   gp_Pnt(p5.X(), p5.Y(), 0)).Edge()
    arc6 = BRepBuilderAPI_MakeEdge(GeomAPI.To3d_(inner_arc, plane)).Edge()

    p6 = inner_arc.EndPoint()
    lin2 = BRepBuilderAPI_MakeEdge(gp_Pnt(p6.X(), p6.Y(), 0),
                                   gp_Pnt(p0.X(), p0.Y(), 0)).Edge()

    wire = BRepBuilderAPI_MakeWire(arc1)
    wire.Add(arc2)
    wire.Add(arc3)
    wire.Add(arc4)
    wire.Add(arc5)
    wire.Add(lin1)
    wire.Add(arc6)
    wire.Add(lin2)

    face = BRepBuilderAPI_MakeFace(wire.Wire())

    wedge = BRepPrimAPI_MakePrism(face.Shape(), gp_Vec(0.0, 0.0, thickness))

    return wedge.Shape()


def round_tooth(wedge):
    round_x = 2.6
    round_z = 0.06 * pitch
    round_radius = pitch

    # Determine where the circle used for rounding has to start and stop
    p2d_1 = gp_Pnt2d(top_radius - round_x, 0)
    p2d_2 = gp_Pnt2d(top_radius, round_z)

    # Construct the rounding circle
    round_circle = GccAna_Circ2d2TanRad(p2d_1, p2d_2, round_radius, 0.01)
    if (round_circle.NbSolutions() != 2):
        sys.exit(-2)

    round_circle_2d_1 = round_circle.ThisSolution(1)
    round_circle_2d_2 = round_circle.ThisSolution(2)

    if (round_circle_2d_1.Position().Location().Coord().Y() >= 0):
        round_circle_2d = round_circle_2d_1
    else:
        round_circle_2d = round_circle_2d_2

    # Remove the arc used for rounding
    trimmed_circle = GCE2d_MakeArcOfCircle(round_circle_2d, p2d_1, p2d_2).Value()

    # Calculate extra points used to construct lines
    p1 = gp_Pnt(p2d_1.X(), 0, p2d_1.Y())
    p2 = gp_Pnt(p2d_2.X(), 0, p2d_2.Y())
    p3 = gp_Pnt(p2d_2.X() + 1, 0, p2d_2.Y())
    p4 = gp_Pnt(p2d_2.X() + 1, 0, p2d_1.Y() - 1)
    p5 = gp_Pnt(p2d_1.X(), 0, p2d_1.Y() - 1)

    # Convert the arc and four extra lines into 3D edges
    plane = gp_Pln(gp_Ax3(gp.Origin_(), gp.DY_().Reversed(), gp.DX_()))
    arc1 = BRepBuilderAPI_MakeEdge(GeomAPI.To3d_(trimmed_circle, plane)).Edge()
    lin1 = BRepBuilderAPI_MakeEdge(p2, p3).Edge()
    lin2 = BRepBuilderAPI_MakeEdge(p3, p4).Edge()
    lin3 = BRepBuilderAPI_MakeEdge(p4, p5).Edge()
    lin4 = BRepBuilderAPI_MakeEdge(p5, p1).Edge()

    # Make a wire composed of the edges
    round_wire = BRepBuilderAPI_MakeWire(arc1)
    round_wire.Add(lin1)
    round_wire.Add(lin2)
    round_wire.Add(lin3)
    round_wire.Add(lin4)

    # Turn the wire into a face
    round_face = BRepBuilderAPI_MakeFace(round_wire.Wire()).Shape()

    # Revolve the face around the Z axis over the tooth angle
    rounding_cut_1 = BRepPrimAPI_MakeRevol(round_face, gp.OZ_(), tooth_angle).Shape()

    # Construct a mirrored copy of the first cutting shape
    mirror = gp_Trsf()
    mirror.SetMirror(gp.XOY_())
    mirrored_cut_1 = BRepBuilderAPI_Transform(rounding_cut_1, mirror, True).Shape()

    # and translate it so that it ends up on the other side of the wedge
    translate = gp_Trsf()
    translate.SetTranslation(gp_Vec(0, 0, thickness))
    rounding_cut_2 = BRepBuilderAPI_Transform(mirrored_cut_1, translate, False).Shape()

    # Cut the wedge using the first and second cutting shape
    cut_1 = BRepAlgoAPI_Cut(wedge, rounding_cut_1).Shape()
    cut_2 = BRepAlgoAPI_Cut(cut_1, rounding_cut_2).Shape()

    return cut_2


def clone_tooth(base_shape):
    clone = gp_Trsf()
    grouped_shape = base_shape

    # Find a divisor, between 1 and 8, for the number_of teeth
    multiplier = 1
    max_multiplier = 1
    for i in range(0, 8):
        if num_teeth % multiplier == 0:
            max_multiplier = i + 1

    multiplier = max_multiplier
    for i in range(1, multiplier):
        clone.SetRotation(gp.OZ_(), -i * tooth_angle)
        rotated_shape = BRepBuilderAPI_Transform(base_shape, clone, True).Shape()
        grouped_shape = BRepAlgoAPI_Fuse(grouped_shape, rotated_shape).Shape()

    # Rotate the basic tooth and fuse together
    aggregated_shape = grouped_shape
    for i in range(1, int(num_teeth / multiplier)):
        clone.SetRotation(gp.OZ_(), - i * multiplier * tooth_angle)
        rotated_shape = BRepBuilderAPI_Transform(grouped_shape, clone, True).Shape()
        aggregated_shape = BRepAlgoAPI_Fuse(aggregated_shape, rotated_shape).Shape()

    cylinder = BRepPrimAPI_MakeCylinder(gp.XOY_(),
                                        top_radius - roller_diameter,
                                        thickness)
    aggregated_shape = BRepAlgoAPI_Fuse(aggregated_shape,
                                        cylinder.Shape()).Shape()

    return aggregated_shape


def center_hole(base):
    cylinder = BRepPrimAPI_MakeCylinder(center_radius, thickness).Shape()
    cut = BRepAlgoAPI_Cut(base, cylinder)
    return cut.Shape()


def mounting_holes(base):
    result = base
    for i in range(0, mounting_hole_count):
        center = gp_Pnt(cos(i * M_PI / 3) * mounting_radius,
                        sin(i * M_PI / 3) * mounting_radius, 0.0)
        center_axis = gp_Ax2(center, gp.DZ_())

        cylinder = BRepPrimAPI_MakeCylinder(center_axis, hole_radius,
                                            thickness).Shape()
        result = BRepAlgoAPI_Cut(result, cylinder).Shape()

        cone = BRepPrimAPI_MakeCone(center_axis,
                                    hole_radius + thickness / 2.,
                                    hole_radius, thickness / 2.)
        result = BRepAlgoAPI_Cut(result, cone.Shape()).Shape()

    return result


def cut_out(base):
    outer = gp_Circ2d(gp.OX2d_(), top_radius - 1.75 * roller_diameter)
    inner = gp_Circ2d(gp.OX2d_(), center_radius + 0.75 * roller_diameter)

    geom_outer = GCE2d_MakeCircle(outer).Value()
    geom_inner = GCE2d_MakeCircle(inner).Value()
    geom_inner.Reverse()

    base_angle = (2. * M_PI) / mounting_hole_count
    hole_angle = atan(hole_radius / mounting_radius)
    correction_angle = 3 * hole_angle

    left = gp_Lin2d(gp.Origin2d_(), gp.DX2d_())
    right = gp_Lin2d(gp.Origin2d_(), gp.DX2d_())
    left.Rotate(gp.Origin2d_(), correction_angle)
    right.Rotate(gp.Origin2d_(), base_angle - correction_angle)

    geom_left = GCE2d_MakeLine(left).Value()
    geom_right = GCE2d_MakeLine(right).Value()

    inter_1 = Geom2dAPI_InterCurveCurve(geom_outer, geom_left)
    inter_2 = Geom2dAPI_InterCurveCurve(geom_outer, geom_right)
    inter_3 = Geom2dAPI_InterCurveCurve(geom_inner, geom_right)
    inter_4 = Geom2dAPI_InterCurveCurve(geom_inner, geom_left)

    if inter_1.Point(1).X() > 0:
        p1 = inter_1.Point(1)
    else:
        p1 = inter_1.Point(2)

    if inter_2.Point(1).X() > 0:
        p2 = inter_2.Point(1)
    else:
        p2 = inter_2.Point(2)

    if inter_3.Point(1).X() > 0:
        p3 = inter_3.Point(1)
    else:
        p3 = inter_3.Point(2)

    if inter_4.Point(1).X() > 0:
        p4 = inter_4.Point(1)
    else:
        p4 = inter_4.Point(2)

    trimmed_outer = GCE2d_MakeArcOfCircle(outer, p1, p2).Value()
    trimmed_inner = GCE2d_MakeArcOfCircle(inner, p4, p3).Value()

    plane = gp_Pln(gp.Origin_(), gp.DZ_())

    arc1 = BRepBuilderAPI_MakeEdge(GeomAPI.To3d_(trimmed_outer, plane)).Edge()

    lin1 = BRepBuilderAPI_MakeEdge(gp_Pnt(p2.X(), p2.Y(), 0),
                                   gp_Pnt(p3.X(), p3.Y(), 0)).Edge()

    arc2 = BRepBuilderAPI_MakeEdge(GeomAPI.To3d_(trimmed_inner, plane)).Edge()

    lin2 = BRepBuilderAPI_MakeEdge(gp_Pnt(p4.X(), p4.Y(), 0),
                                   gp_Pnt(p1.X(), p1.Y(), 0)).Edge()

    cutout_wire = BRepBuilderAPI_MakeWire(arc1)
    cutout_wire.Add(lin1)
    cutout_wire.Add(arc2)
    cutout_wire.Add(lin2)

    # Turn the wire into a face
    cutout_face = BRepBuilderAPI_MakeFace(cutout_wire.Wire())
    filleted_face = BRepFilletAPI_MakeFillet2d(cutout_face.Face())

    explorer = BRepTools_WireExplorer(cutout_wire.Wire())
    while explorer.More():
        vertex = explorer.CurrentVertex()
        filleted_face.AddFillet(vertex, roller_radius)
        explorer.Next()

    cutout = BRepPrimAPI_MakePrism(filleted_face.Shape(),
                                   gp_Vec(0.0, 0.0, thickness)).Shape()

    result = base
    rotate = gp_Trsf()
    for i in range(0, mounting_hole_count):
        rotate.SetRotation(gp.OZ_(), i * 2. * M_PI / mounting_hole_count)
        rotated_cutout = BRepBuilderAPI_Transform(cutout, rotate, True)

        result = BRepAlgoAPI_Cut(result,
                                 rotated_cutout.Shape()).Shape()

    return result


class Sprocket(RawShape):
    def create_shape(self, parent):
        # create the sprocket model
        wedge = build_tooth()
        rounded_wedge = round_tooth(wedge)
        basic_disk = clone_tooth(rounded_wedge)
        cut_disc = center_hole(basic_disk)
        mountable_disc = mounting_holes(cut_disc)
        sprocket = cut_out(mountable_disc)
        return sprocket

Now the custom shape can be used in DeclaraCAD like any other declaration

enamldef Assembly(Part):
    Sprocket: sprocket:
        material = 'steel'

RawShape sprocket example

Custom shapes will also then be able to use the topology and work with boolean operations, etc.

RawPart is used just like RawShape except RawPart can should implement the get_shapes method and return a list of TopoDS_Shape.

Adding attributes

You can define attributes that your custom shape can accept when declared from enaml by defining new members on the subclass.

To make it "bindable" from enaml it must be tagged as a member by calling d_ on the member as shown below:

from atom.api import Float
from enaml.core.declarative import d_

class Sprocket(RawShape):
    # This can now be set from enaml
    radius = d_(Float(1.0, strict=False))

    def create_shape(self, parent):
         r = self.radius
         # etc...

Then users can set the attribute from enaml

Sprocket:
   radius = 5.0

Note: enaml only allows you to set fields that are defined as "declarative members" as shown above. Attempting to assign anything that is not a member will raise an error.

Accessing children

The RawShape subclass can also access the object tree (to create a shape based on it's children for example). Shapes all can use the enaml object API: enaml.core.object.