DeclaraCAD is built on the OpenCascade modeling kernel and aims to provide higher level and easier to use abstractions on top of OpenCascade. To do this DeclaraCAD uses pyOCCT and enaml (an extension to python) to define 3D models programmatically.

DeclaraCAD is a standalone Qt desktop application. By design it does not require any internet access and is not a service as a software substitute (SAAS). Models are simple text files that can be copied, shared, and managed with version control and are fully owned and controlled by you.

DeclaraCAD 2022

DeclaraCAD has been in development since 2016 see credits on all those how help make it possible.

Introduction

Many 3D applications show the set of operations used to build a shape or part in a tree, with the outer most part (the root) of the tree being the final part or shape produced.

In DeclaraCAD this tree written in code instead of created using the application itself. Since DeclaraCAD code is defined in a tree it is arguably easier to read than other alternatives like CadQuery or python-occ which use python directly, although this depends on the user's preferences.

Enaml was chosen to avoid needing to chain long expressions like CadQuery does. Enaml is a python based DSL which enables declaring tree like structures in code.

Illustrative example

For example, the classical OpenCascade bottle example written using python-occ is shown below:

##Copyright 2009-2015 Thomas Paviot (tpaviot@gmail.com)
##
##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/>.

import math

from OCC.Core.gp import (gp_Pnt, gp_OX, gp_Vec, gp_Trsf, gp_DZ, gp_Ax2, gp_Ax3,
                         gp_Pnt2d, gp_Dir2d, gp_Ax2d)
from OCC.Core.GC import GC_MakeArcOfCircle, GC_MakeSegment
from OCC.Core.GCE2d import GCE2d_MakeSegment
from OCC.Core.Geom import Geom_CylindricalSurface
from OCC.Core.Geom2d import Geom2d_Ellipse, Geom2d_TrimmedCurve
from OCC.Core.BRepBuilderAPI import (BRepBuilderAPI_MakeEdge, BRepBuilderAPI_MakeWire,
                                     BRepBuilderAPI_MakeFace, BRepBuilderAPI_Transform)
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakePrism, BRepPrimAPI_MakeCylinder
from OCC.Core.BRepFilletAPI import BRepFilletAPI_MakeFillet
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Fuse
from OCC.Core.BRepOffsetAPI import BRepOffsetAPI_MakeThickSolid, BRepOffsetAPI_ThruSections
from OCC.Core.BRepLib import breplib
from OCC.Core.BRep import BRep_Builder
from OCC.Core.GeomAbs import GeomAbs_Plane
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
from OCC.Core.TopoDS import topods, TopoDS_Compound
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_EDGE, TopAbs_FACE
from OCC.Core.TopTools import TopTools_ListOfShape

def face_is_plane(face):
    """
    Returns True if the TopoDS_Shape is a plane, False otherwise
    """
    surf = BRepAdaptor_Surface(face, True)
    surf_type = surf.GetType()
    return surf_type == GeomAbs_Plane


def geom_plane_from_face(aFace):
    """
    Returns the geometric plane entity from a planar surface
    """
    return BRepAdaptor_Surface(aFace, True).Plane()


height = 70
width = 50
thickness = 30

print("creating bottle")
# The points we'll use to create the profile of the bottle's body
aPnt1 = gp_Pnt(-width / 2.0, 0, 0)
aPnt2 = gp_Pnt(-width / 2.0, -thickness / 4.0, 0)
aPnt3 = gp_Pnt(0, -thickness / 2.0, 0)
aPnt4 = gp_Pnt(width / 2.0, -thickness / 4.0, 0)
aPnt5 = gp_Pnt(width / 2.0, 0, 0)

aArcOfCircle = GC_MakeArcOfCircle(aPnt2, aPnt3, aPnt4)
aSegment1 = GC_MakeSegment(aPnt1, aPnt2)
aSegment2 = GC_MakeSegment(aPnt4, aPnt5)

# Could also construct the line edges directly using the points instead of the resulting line
aEdge1 = BRepBuilderAPI_MakeEdge(aSegment1.Value())
aEdge2 = BRepBuilderAPI_MakeEdge(aArcOfCircle.Value())
aEdge3 = BRepBuilderAPI_MakeEdge(aSegment2.Value())

# Create a wire out of the edges
aWire = BRepBuilderAPI_MakeWire(aEdge1.Edge(), aEdge2.Edge(), aEdge3.Edge())

# Quick way to specify the X axis
xAxis = gp_OX()

# Set up the mirror
aTrsf = gp_Trsf()
aTrsf.SetMirror(xAxis)

# Apply the mirror transformation
aBRespTrsf = BRepBuilderAPI_Transform(aWire.Wire(), aTrsf)

# Get the mirrored shape back out of the transformation and convert back to a wire
aMirroredShape = aBRespTrsf.Shape()

# A wire instead of a generic shape now
aMirroredWire = topods.Wire(aMirroredShape)

# Combine the two constituent wires
mkWire = BRepBuilderAPI_MakeWire()
mkWire.Add(aWire.Wire())
mkWire.Add(aMirroredWire)
myWireProfile = mkWire.Wire()

# The face that we'll sweep to make the prism
myFaceProfile = BRepBuilderAPI_MakeFace(myWireProfile)

# We want to sweep the face along the Z axis to the height
aPrismVec = gp_Vec(0, 0, height)
myBody = BRepPrimAPI_MakePrism(myFaceProfile.Face(), aPrismVec)

# Add fillets to all edges through the explorer
mkFillet = BRepFilletAPI_MakeFillet(myBody.Shape())
anEdgeExplorer = TopExp_Explorer(myBody.Shape(), TopAbs_EDGE)

while anEdgeExplorer.More():
    anEdge = topods.Edge(anEdgeExplorer.Current())
    mkFillet.Add(thickness / 12.0, anEdge)

    anEdgeExplorer.Next()

myBody = mkFillet

# Create the neck of the bottle
neckLocation = gp_Pnt(0, 0, height)
neckAxis = gp_DZ()
neckAx2 = gp_Ax2(neckLocation, neckAxis)

myNeckRadius = thickness / 4.0
myNeckHeight = height / 10.0

mkCylinder = BRepPrimAPI_MakeCylinder(neckAx2, myNeckRadius, myNeckHeight)

myBody = BRepAlgoAPI_Fuse(myBody.Shape(), mkCylinder.Shape())

# Our goal is to find the highest Z face and remove it
faceToRemove = None
zMax = -1

# We have to work our way through all the faces to find the highest Z face so we can remove it for the shell
aFaceExplorer = TopExp_Explorer(myBody.Shape(), TopAbs_FACE)
while aFaceExplorer.More():
    aFace = topods.Face(aFaceExplorer.Current())

    if face_is_plane(aFace):
        aPlane = geom_plane_from_face(aFace)

        # We want the highest Z face, so compare this to the previous faces
        aPnt = aPlane.Location()
        aZ = aPnt.Z()
        if aZ > zMax:
            zMax = aZ
            faceToRemove = aFace

    aFaceExplorer.Next()

facesToRemove = TopTools_ListOfShape()
facesToRemove.Append(faceToRemove)

myBody = BRepOffsetAPI_MakeThickSolid(myBody.Shape(), facesToRemove, -thickness / 50.0, 0.001)

# Set up our surfaces for the threading on the neck
neckAx2_Ax3 = gp_Ax3(neckLocation, gp_DZ())
aCyl1 = Geom_CylindricalSurface(neckAx2_Ax3, myNeckRadius * 0.99)
aCyl2 = Geom_CylindricalSurface(neckAx2_Ax3, myNeckRadius * 1.05)

# Set up the curves for the threads on the bottle's neck
aPnt = gp_Pnt2d(2.0 * math.pi, myNeckHeight / 2.0)
aDir = gp_Dir2d(2.0 * math.pi, myNeckHeight / 4.0)
anAx2d = gp_Ax2d(aPnt, aDir)

aMajor = 2.0 * math.pi
aMinor = myNeckHeight / 10.0

anEllipse1 = Geom2d_Ellipse(anAx2d, aMajor, aMinor)
anEllipse2 = Geom2d_Ellipse(anAx2d, aMajor, aMinor / 4.0)

anArc1 = Geom2d_TrimmedCurve(anEllipse1, 0, math.pi)
anArc2 = Geom2d_TrimmedCurve(anEllipse2, 0, math.pi)

anEllipsePnt1 = anEllipse1.Value(0)
anEllipsePnt2 = anEllipse1.Value(math.pi)

aSegment = GCE2d_MakeSegment(anEllipsePnt1, anEllipsePnt2)

# Build edges and wires for threading
anEdge1OnSurf1 = BRepBuilderAPI_MakeEdge(anArc1, aCyl1)
anEdge2OnSurf1 = BRepBuilderAPI_MakeEdge(aSegment.Value(), aCyl1)
anEdge1OnSurf2 = BRepBuilderAPI_MakeEdge(anArc2, aCyl2)
anEdge2OnSurf2 = BRepBuilderAPI_MakeEdge(aSegment.Value(), aCyl2)

threadingWire1 = BRepBuilderAPI_MakeWire(anEdge1OnSurf1.Edge(), anEdge2OnSurf1.Edge())
threadingWire2 = BRepBuilderAPI_MakeWire(anEdge1OnSurf2.Edge(), anEdge2OnSurf2.Edge())

# Compute the 3D representations of the edges/wires
breplib.BuildCurves3d(threadingWire1.Shape())
breplib.BuildCurves3d(threadingWire2.Shape())

# Create the surfaces of the threading
aTool = BRepOffsetAPI_ThruSections(True)
aTool.AddWire(threadingWire1.Wire())
aTool.AddWire(threadingWire2.Wire())
aTool.CheckCompatibility(False)
myThreading = aTool.Shape()

# Build the resulting compound
bottle = TopoDS_Compound()
aBuilder = BRep_Builder()
aBuilder.MakeCompound(bottle)
aBuilder.Add(bottle, myBody.Shape())
aBuilder.Add(bottle, myThreading)
print("bottle finished")

The CadQuery version is here classic-occ-bottle which is much more simple but it does not implement the threads.

Now contrast this with the equivalent DeclaraCAD bottle model:

"""
See https://dev.opencascade.org/doc/overview/html/occt__tutorial.html
for more details on how this works.
"""
from math import pi
from declaracad.occ.api import *


enamldef Assembly(Part): part:
    name = "Bottle"

    #: "Parametric" properties of this shape
    attr height = 70.0
    attr width = 50.0
    attr thickness = 30.0

    func make_cylinder_surface(**kwargs):
        cylinder = Cylinder(**kwargs)
        cylinder.render()
        return cylinder.topology.faces[0]

    Fuse:
      color = '#abc'
      transparency = 0.4

      ThickSolid: bottle:
        # Hollows out the bottle
        #faces << [neck.shape_faces[0]]
        offset << thickness/50.0
        Fuse:
            # Fuse the bottle to the neck
            Cylinder: neck:
                # Bottle neck
                position << (0,0,part.height)
                direction = (0,0,1)
                radius << thickness/4.0
                height << part.height/10.0
            Fillet: body:
                # Bottle, with filleted edges
                radius << thickness/12.0
                Prism:
                    # Create a solid from the bottle face
                    vector << (0,0,height)
                    Face:
                        # Create a face from the base profile
                        Wire:
                            # Create a wire from the profile and mirrored profile
                            Wire: profile:
                                Segment: s1:
                                    points << [ (-width/2.0, 0, 0), (-width/2.0, -thickness/4.0, 0)]
                                Arc:
                                    points = [s1.points[-1],
                                                    (0, -thickness/2.0, 0) ,
                                                    s2.points[-1]]
                                Segment: s2:
                                    points = [(width/2.0, 0, 0), (width/2.0, -thickness/4.0, 0) ]
                            Transform:
                                #: TODO coerce
                                operations = [Mirror(x=1)]
                                shape = profile


      ThruSections: threads:
        solid = True
        Wire: w1:
            attr r = neck.radius + bottle.offset
            TrimmedCurve: c1:
                surface = make_cylinder_surface(radius=r*0.99, position=neck.position)
                v = pi
                Ellipse: e1:
                    position = (2*pi, neck.height/2)
                    rotation = (pi/8, neck.height/4)
                    major_radius = 3*pi
                    minor_radius = neck.height / 10
            Segment: s3:
               surface = c1.surface
               points = [e1.get_value_at(0), e1.get_value_at(pi)]
        Wire:
            color = 'green'
            TrimmedCurve: c2:
                surface = make_cylinder_surface(radius=w1.r*1.05, position=neck.position)
                v = pi
                Ellipse: e2:
                    position = e1.position
                    rotation = e1.rotation
                    major_radius = e1.major_radius
                    minor_radius = e1.minor_radius/4
            Segment: s4:
                surface = c2.surface
                points = [e2.get_value_at(0), e2.get_value_at(pi)]

Here's the result

DeclaraCAD OpenCascade Bottle Example

These both build the same shape but since DeclaraCAD is written using a tree structure it can be faster to tell at first glance what is being built and the processes used to build it.

It's important to point out that DeclaraCAD is providing a lot of abstractions here by converting higher level declarations of things like the Ellipse to the underlying Geom2d_Ellipse and BRepBuilderAPI_MakeEdge etc.. These complex names and underlying representations of shapes cause confusion and require a lot more knowledge of the underlying frameworks than should be required to build a model.

The actual code needed to build the bottle (shown in the python-occ example code above) is quite confusing without an intricate knowledge of OpenCascade (and even then is still confusing). DeclaraCAD tries to reduce the burden here.

On the flip side however, abstractions cannot always know what you want and solve every use case, DeclaraCAD also provides a way for you to pull in code that directly uses the underlying OpenCascade API's to handle cases that the abstractions don't support.