Skip to content

Direct API Proposal

thebluedirt edited this page Sep 8, 2022 · 2 revisions

Exploring a Direct api for CQ

Definitions

  1. the existing fluent api refers to the CQ 2.0 fluent api, workplane() and such
  2. The shape api refers to the current CQ2.0 methods available on the Shape classes, such as makeEdge
  3. The OCP api or OCP is the current python bindings to OpenCascade
  4. The operation api refers to the way of working described here
  5. The new fluent api refers to a new, probably fluent api designed for end users, that's easier to use than the operation api, and the existing fluent api. the new fluent api should be implemented in terms of the direct api, so that the concepts are the same
  6. Topology Objects refer to the CQ wrappers for OCP Topological entities Edge, Face, Solid, Shell, Vertex, and so-on

Goals

These are the qualities we are trying to achieve with the new direct api

  1. Don't break existing fluent or shape apis
  2. Consistency
  3. Easy for new users to use and understand
  4. Easy for third parties to extend
  5. No magic ( see #3 )
  6. Support finding out what an operation did, includng selecting created, modified topological objects

Design Rules

Here are some design rules I think will help achieve our goals

  1. Use keyword arguments, so that code is more readable to newcomers
  2. Non-fluent, minimizing the number of concepts to understand
  3. No hidden context or state. This makes the api what you see is what you get.
  4. Use tuples intsead of vector for coordinates, since these are especially useful in 2D
  5. Avoid side affects -- the user should be able to assign topology objects to a variable, and it should not change unless it is re-asserted
  6. Avoid Compound. this is an OCP/OCC aggregating topology type that confuses things considerably
  7. Avoid Wire. this is an OCP/OCC aggregating topology type that confuses things too
  8. PEP 484 and PEP 8 (this is new code)
  9. Design for unit testing. We shouldnt need to do much if any mocking or setup to test operations.
  10. Provide scaffolding for third party operations.
  11. Third parties should be able to provide operations that become available in both the operation api AND the new fluent api without the need for monkey patching
  12. Problems should raise Exceptions, but not Exceptions from the OCP layer (IE, no leaking OCP errors ) 11 use regular lists and sets. This allows pythonic code to find items as needed

Design Challenges

Here are some challenges that presented themselves during design. Some of these influenced the design considerably

  1. Face/Edge Confusion Users are familiar with geometric shapes like Rectangle and Circle, so they are commonly used to describe these shapes. Unfortunately, in OCP/OCC, a Rectangle can refer to a face ( IE, it is closed), a Wire( a linked set of edges), or a list of edges.
    Sometimes, its not clear which a user intends, and dis-ambiguating them is tricky.
    The shape api does it by defining two methods: Face.makeCircle() creates a circular FACE, while Edge.makeCircle() creates an edge

  2. A 2D workplane is state It is fairly intuititve for a user to imagine the objects being created as operating on a selected 2D plane. In that case, the user supplies 2D coordinates, so that the shapes are created in the selected plane. Since the user will do several operations on the same plane, it is syntactically nicer to allow the user to supply the plane a single time, vs in every call. Unfortunately, this requires saving state, that increases the conceptual complexity

  3. Patterning Users should be able to replicate an operatoin based on a variety of patterns, such as a rectangular grid. It should be possible to pattern an Operation, but also an arbitrary user-provided set of code ( an anonymous, inline operation, if you will).

  4. Chaining Constructors Constructors are a good way to require inputs-- only one line of user code is needed, and we can avoid states where we don't have valid arguments. We can use superclasses to provide inherited functionality, but there's a problem: once we have a lot of superclasses, we start running into a lot of boilerplate superclass calls. It is nice to minimize this if we can

  5. It's hard to imagine constraints without state Constraint based modelling ( assemblies or sketches) implies storing a list of constraints and then solving them at the end-- which makes it feel a little weird to model as a stateless operation

  6. When should the work be done? When should the operation actually do its work? The most obvious choice is in the constructor, because then the user can't begin calling interrogation methods without the operation having been completed. That prevents passing an operation from one to another, though, which could be a useful way to support deferred execution. A build() method makes it very clear when the work is done, and would support deferred execution, but then the user has to call the extra method

Proposed Operation Api

Concepts

  1. Operations accept paramters in their constructors
  2. Operations return topological entities when they complete.
  3. Operations should expose methods to allow interrogating what happened
  4. The operation class hierarchy is used to:
    • make it clear to people USING operations how they should be used
    • make it clear to people IMPLEMENTING operations how to re-use existing code
    • provide scaffolding to make new operations easy to create

Things I'm unhappy with

  1. Patterns are kind of icky. For one, i dont like the somewhat odd flow of events created when you call the constructor of an operation once, but then repeatedly call set_reference_point followed by methods like wire() or solid() to get the result. this can be cleaned up if all operations have a perform() method
import cadquery as cq
from abc import ABC
from enum import Enum
from cadquery import (
    Edge,
    Vertex,
    Solid,
    Face,
    Wire,
    Workplane,
    Plane
)
from typing import (
    overload,
    Sequence,
    TypeVar,
    Union,
    Tuple,
    Optional,
    Any,
    Iterable,
    Callable,
    Generic,
    List,
    cast,
    Dict,
)


NUMERIC = Union[int,float]
TUPLE_2D = Tuple[NUMERIC,NUMERIC]
TUPLE_3D = Tuple[NUMERIC,NUMERIC,NUMERIC]
ANY_SHAPE = Union[Wire, Vertex, Solid, Edge]
SINGLE_SHAPE = TypeVar("ONE_SHAPE", Wire, Face, Vertex, Solid, Edge)
ORIGIN_3D = (0,0,0)
ORIGIN_2D = (0,0)
DX = (1, 0, 0)
DY = (0,1,0)
DZ = ( 0, 0, 1)

class ResultFilter(Enum):
    ALL = 0,
    CREATED = 0
    REUSED = 1


class BaseOperation(ABC):
    def __init__(tag:str =None):
        self.tag = tag
        
    def generated(t: SINGLE_SHAPE, filter:ResultFilter = ResultFilter.ALL) -> List[SINGLE_SHAPE]:
        pass

class EdgeOperation(BaseOperation):
    
    def edge() -> List[Edge]:
        pass
    
class WireOperation(BaseOperation):

    def wire() -> Wire:
        pass

class FaceOperation(BaseOperation):
    def face() -> Face:
        pass
    
    def outer_wire() -> Wire:
        pass
    

class SolidOperation(BaseOperation):
    def solid() -> Solid:
        pass

class PlanarOperation(BaseOperation):
    def __init__(self,workplane:Workplane):
        self.workplane = workplane
        
class CornerRectangle(PlanarOperation,WireOperation):
    def __init__(self,workplane:Workplane, width:NUMERIC, height:NUMERIC, center:TUPLE_2D = ORIGIN_2D ):
        super.__init__(workplane=workplane)
        self.center=center
        self.width=width
        self.height=height

class PolyLine(PlanarOperation,WireOperation):
    def __init__(self,workplane:Workplane, points:List[TUPLE_2D]):
        super.__init__(workplane=workplane)

class Line(PlanarOperation,EdgeOperation):
    def __init__(self,workplane:Workplane, from:TUPLE_2D = (0,0), to:TUPLE_2D = (0,0) ):
        super.__init__(workplane=workplane)

    def edge(self) -> Edge:
        pass

class LocatedOperation(ABC):
    #mixin
    def set_reference_point(reference_point:TUPLE_3D=ORIGIN_3D):
        pass

class CloseEdges(FaceOperation):
    def __init__(edges_in_a_loop: List[Edge]):
        self.edges = edges_in_a_loop
    
    def face() -> Face:
        pass
    
    def outer_wire() -> Wire:
        pass

class HullEdges(FaceOperation):
    def __init__(edges: List[Edge]):
        self.edges = edges
    
    def face() -> Face:
        pass
    
    def wire() -> Wire:
        pass

    def outer_wire() -> Wire:
        pass

class LinearExtrude(SolidOperation):
    def __init__(dir:TUPLE_3D, faces: List[Face]):
        pass
    
class Helix(WireOperation):
    def __init__(pitch: NUMERIC, height:NUMERIC, radius:NUMERIC, center:TUPLE_3D=ORIGIN_3D, dir:TUPLE_3D = DZ, left_hand:bool = False):
        pass

    def wire() -> Wire:
        pass

class Mirror(Generic[SINGLE_SHAPE]):
    def __init__(mirror_plane:Plane, to_mirror: SINGLE_SHAPE):
        pass
    
    def mirrored() -> SINGLE_SHAPE:
        pass
    
class Copy(Generic[SINGLE_SHAPE]):
    def __init__(to_copy:SINGLE_SHAPE):
        pass
    
    def copied()-> SINGLE_SHAPE:
        pass
        

class ProjectShape(BaseOperation):
    def __init__(workplane:Workplane, to_project: SINGLE_SHAPE):
        pass
    
    def projected() -> SINGLE_SHAPE:
        pass

class LoftedSolid(SolidOperation):
    def __init__(profiles: List[Wire], ruled: bool = False):
        pass

    def solid() -> Solid:
        pass
    
    
class LoftedFace(FaceOperation):
    def __init__(boundaries:List[Edge]):
        pass

    def __init__(edges_in_a_loop: List[Edge]):
        self.edges = edges_in_a_loop
    
    def face() -> Face:
        pass
    
    def outer_wire() -> Wire:
        pass    
    
class CutOperation(SolidOperation):
    def __init__(base:Solid, to_cut:List[Solid]):
        pass

    def solid() -> Solid:
        pass

class SolidFillet(SolidOperation):
    def __init__(to_fillet: Solid, edges: List[Edge], radius:NUMERIC):
        pass        

    def solid() -> Solid:
        pass

class PrimitiveSolid(SolidOperation,LocatedOperation):
    #marker
    def set_reference_point(reference_point:TUPLE_3D=ORIGIN_3D):
        pass

class Sphere(PrimitiveSolid):
    def __init__(radius: NUMERIC, center:TUPLE_3D = ORIGIN_3D, angle_1:NUMERIC=0, angle_2:NUMERIC=90, angle_3:NUMERIC=360):
        pass

    def solid() -> Solid:
        pass

class PointPattern():
    def __init__(points:List[TUPLE_3D], to_pattern:LocatedOperation ):
        self.points = points
        self.to_pattern = to_pattern
        
    def patterned() -> List[Shape]:
        r : List[Shape]
        for p in self.points:
            #i dont like this. it implies that operations have to detect a new reference point and re-calculate results
            self.to_pattern.set_reference_point(p)
            r.append(self.to_pattern.generated())
        return r
            

Examples

Example of creating an edge between two points

     p = Plane.XY
     e = Line(plane=p, from=(0,0), to=(1,1)).edge()