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 has been in development since 2016 see credits on all those how help make it possible.
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.
For example, the classical OpenCascade bottle example written using python-occ is shown below:
##Copyright 2009-2015 Thomas Paviot (firstname.lastname@example.org) ## ##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 Fuse: color = '#abc' transparency = 0.4 ThickSolid: bottle: # Hollows out the bottle #faces << [neck.shape_faces] 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
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
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.