-
Notifications
You must be signed in to change notification settings - Fork 295
Direct API Proposal
- the existing fluent api refers to the CQ 2.0 fluent api, workplane() and such
- The shape api refers to the current CQ2.0 methods available on the Shape classes, such as makeEdge
- The OCP api or OCP is the current python bindings to OpenCascade
- The operation api refers to the way of working described here
- 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
- Topology Objects refer to the CQ wrappers for OCP Topological entities Edge, Face, Solid, Shell, Vertex, and so-on
These are the qualities we are trying to achieve with the new direct api
- Don't break existing fluent or shape apis
- Consistency
- Easy for new users to use and understand
- Easy for third parties to extend
- No magic ( see #3 )
- Support finding out what an operation did, includng selecting created, modified topological objects
Here are some design rules I think will help achieve our goals
- Use keyword arguments, so that code is more readable to newcomers
- Non-fluent, minimizing the number of concepts to understand
- No hidden context or state. This makes the api what you see is what you get.
- Use tuples intsead of vector for coordinates, since these are especially useful in 2D
- 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
- Avoid Compound. this is an OCP/OCC aggregating topology type that confuses things considerably
- Avoid Wire. this is an OCP/OCC aggregating topology type that confuses things too
- PEP 484 and PEP 8 (this is new code)
- Design for unit testing. We shouldnt need to do much if any mocking or setup to test operations.
- Provide scaffolding for third party operations.
- 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
- 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
Here are some challenges that presented themselves during design. Some of these influenced the design considerably
-
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 -
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
-
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).
-
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
-
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
-
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
- Operations accept paramters in their constructors
- Operations return topological entities when they complete.
- Operations should expose methods to allow interrogating what happened
- 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
- 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
Example of creating an edge between two points
p = Plane.XY
e = Line(plane=p, from=(0,0), to=(1,1)).edge()