Free function api (#1469)
* Initial free functions * Mypy fixes * Add a mypy plugin that handles _get* * More helpers and fixes * black * More hooks * More ops and primitives * Fill with constraints and cap * Minimal docstrings and mypy fix * Bool op operators for Shape * Extra docstring * Added spline primitive * Added alternative constructors * Update solid * Add shape normalization * Add text * Added moved overload * Another moved overload * Convert location constructor to multimethod * Additional Loc constructor * Extra vertex constructor * Additional cone overload * Start with tests * Fix compouund normalization * Bool op tests * Additional Location overload * test moved and fix bool ops * Different cap params * More tests * Test revolve and offset * Test sweep and loft * Add bool ops * More tests * Test text * Improve coverage for utils * More move[d] and Location overloads * Start working on some docs * Update index * Doc fix * Typo fix * More move/moved overloads * Small doc update * Better Location coverage * Fix angle units in Location * More docs and a usability fix * Cosmetics * Mypy fix * Remove dead code * Coverage tweaks * More docs' * Box centering and box/plane arg order * Docs cosmetics - nicer sweep * Apply suggestions Co-authored-by: Jeremy Wright <wrightjmf@gmail.com> * Add docstrings * Doc tweaks * Bump multimethod version * Add occ_impl.shapes * Mention free funcs in the primer * Typos * Typo * Punctuation --------- Co-authored-by: Jeremy Wright <wrightjmf@gmail.com>
This commit is contained in:
@ -1,4 +1,4 @@
|
|||||||
import math
|
from math import pi, radians, degrees
|
||||||
|
|
||||||
from typing import overload, Sequence, Union, Tuple, Type, Optional
|
from typing import overload, Sequence, Union, Tuple, Type, Optional
|
||||||
|
|
||||||
@ -14,6 +14,8 @@ from OCP.gp import (
|
|||||||
gp_XYZ,
|
gp_XYZ,
|
||||||
gp_EulerSequence,
|
gp_EulerSequence,
|
||||||
gp,
|
gp,
|
||||||
|
gp_Quaternion,
|
||||||
|
gp_Extrinsic_XYZ,
|
||||||
)
|
)
|
||||||
from OCP.Bnd import Bnd_Box
|
from OCP.Bnd import Bnd_Box
|
||||||
from OCP.BRepBndLib import BRepBndLib
|
from OCP.BRepBndLib import BRepBndLib
|
||||||
@ -22,6 +24,7 @@ from OCP.TopoDS import TopoDS_Shape
|
|||||||
from OCP.TopLoc import TopLoc_Location
|
from OCP.TopLoc import TopLoc_Location
|
||||||
|
|
||||||
from ..types import Real
|
from ..types import Real
|
||||||
|
from ..utils import multimethod
|
||||||
|
|
||||||
TOL = 1e-2
|
TOL = 1e-2
|
||||||
|
|
||||||
@ -682,7 +685,7 @@ class Plane(object):
|
|||||||
# NB: this is not a geometric Vector
|
# NB: this is not a geometric Vector
|
||||||
rotate = Vector(rotate)
|
rotate = Vector(rotate)
|
||||||
# Convert to radians.
|
# Convert to radians.
|
||||||
rotate = rotate.multiply(math.pi / 180.0)
|
rotate = rotate.multiply(pi / 180.0)
|
||||||
|
|
||||||
# Compute rotation matrix.
|
# Compute rotation matrix.
|
||||||
T1 = gp_Trsf()
|
T1 = gp_Trsf()
|
||||||
@ -848,11 +851,11 @@ class BoundBox(object):
|
|||||||
|
|
||||||
def enlarge(self, tol: float) -> "BoundBox":
|
def enlarge(self, tol: float) -> "BoundBox":
|
||||||
"""Returns a modified (expanded) bounding box, expanded in all
|
"""Returns a modified (expanded) bounding box, expanded in all
|
||||||
directions by the tolerance value.
|
directions by the tolerance value.
|
||||||
|
|
||||||
This means that the minimum values of its X, Y and Z intervals
|
This means that the minimum values of its X, Y and Z intervals
|
||||||
of the bounding box are reduced by the absolute value of tol, while
|
of the bounding box are reduced by the absolute value of tol, while
|
||||||
the maximum values are increased by the same amount.
|
the maximum values are increased by the same amount.
|
||||||
"""
|
"""
|
||||||
tmp = Bnd_Box()
|
tmp = Bnd_Box()
|
||||||
tmp.Add(self.wrapped)
|
tmp.Add(self.wrapped)
|
||||||
@ -942,75 +945,91 @@ class Location(object):
|
|||||||
|
|
||||||
wrapped: TopLoc_Location
|
wrapped: TopLoc_Location
|
||||||
|
|
||||||
@overload
|
@multimethod
|
||||||
def __init__(self) -> None:
|
|
||||||
"""Empty location with not rotation or translation with respect to the original location."""
|
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def __init__(self, t: VectorLike) -> None:
|
def __init__(self, t: VectorLike) -> None:
|
||||||
"""Location with translation t with respect to the original location."""
|
"""Location with translation t with respect to the original location."""
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
T = gp_Trsf()
|
||||||
def __init__(self, t: Plane) -> None:
|
T.SetTranslationPart(Vector(t).wrapped)
|
||||||
"""Location corresponding to the location of the Plane t."""
|
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
self.wrapped = TopLoc_Location(T)
|
||||||
def __init__(self, t: Plane, v: VectorLike) -> None:
|
|
||||||
"""Location corresponding to the angular location of the Plane t with translation v."""
|
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
@__init__.register
|
||||||
def __init__(self, t: TopLoc_Location) -> None:
|
def __init__(
|
||||||
"""Location wrapping the low-level TopLoc_Location object t"""
|
self,
|
||||||
...
|
x: Real = 0,
|
||||||
|
y: Real = 0,
|
||||||
@overload
|
z: Real = 0,
|
||||||
def __init__(self, t: gp_Trsf) -> None:
|
rx: Real = 0,
|
||||||
"""Location wrapping the low-level gp_Trsf object t"""
|
ry: Real = 0,
|
||||||
...
|
rz: Real = 0,
|
||||||
|
) -> None:
|
||||||
@overload
|
"""Location with translation (x,y,z) and 3 rotation angles."""
|
||||||
def __init__(self, t: VectorLike, ax: VectorLike, angle: float) -> None:
|
|
||||||
"""Location with translation t and rotation around ax by angle
|
|
||||||
with respect to the original location."""
|
|
||||||
...
|
|
||||||
|
|
||||||
def __init__(self, *args):
|
|
||||||
|
|
||||||
T = gp_Trsf()
|
T = gp_Trsf()
|
||||||
|
|
||||||
if len(args) == 0:
|
q = gp_Quaternion()
|
||||||
pass
|
q.SetEulerAngles(gp_Extrinsic_XYZ, radians(rx), radians(ry), radians(rz))
|
||||||
elif len(args) == 1:
|
|
||||||
t = args[0]
|
|
||||||
|
|
||||||
if isinstance(t, (Vector, tuple)):
|
T.SetRotation(q)
|
||||||
T.SetTranslationPart(Vector(t).wrapped)
|
T.SetTranslationPart(Vector(x, y, z).wrapped)
|
||||||
elif isinstance(t, Plane):
|
|
||||||
cs = gp_Ax3(t.origin.toPnt(), t.zDir.toDir(), t.xDir.toDir())
|
self.wrapped = TopLoc_Location(T)
|
||||||
T.SetTransformation(cs)
|
|
||||||
T.Invert()
|
@__init__.register
|
||||||
elif isinstance(t, TopLoc_Location):
|
def __init__(self, t: Plane) -> None:
|
||||||
self.wrapped = t
|
"""Location corresponding to the location of the Plane t."""
|
||||||
return
|
|
||||||
elif isinstance(t, gp_Trsf):
|
T = gp_Trsf()
|
||||||
T = t
|
T.SetTransformation(gp_Ax3(t.origin.toPnt(), t.zDir.toDir(), t.xDir.toDir()))
|
||||||
else:
|
T.Invert()
|
||||||
raise TypeError("Unexpected parameters")
|
|
||||||
elif len(args) == 2:
|
self.wrapped = TopLoc_Location(T)
|
||||||
t, v = args
|
|
||||||
cs = gp_Ax3(Vector(v).toPnt(), t.zDir.toDir(), t.xDir.toDir())
|
@__init__.register
|
||||||
T.SetTransformation(cs)
|
def __init__(self, t: Plane, v: VectorLike) -> None:
|
||||||
T.Invert()
|
"""Location corresponding to the angular location of the Plane t with translation v."""
|
||||||
else:
|
|
||||||
t, ax, angle = args
|
T = gp_Trsf()
|
||||||
T.SetRotation(
|
T.SetTransformation(gp_Ax3(Vector(v).toPnt(), t.zDir.toDir(), t.xDir.toDir()))
|
||||||
gp_Ax1(Vector().toPnt(), Vector(ax).toDir()), angle * math.pi / 180.0
|
T.Invert()
|
||||||
)
|
|
||||||
T.SetTranslationPart(Vector(t).wrapped)
|
self.wrapped = TopLoc_Location(T)
|
||||||
|
|
||||||
|
@__init__.register
|
||||||
|
def __init__(self, T: TopLoc_Location) -> None:
|
||||||
|
"""Location wrapping the low-level TopLoc_Location object t"""
|
||||||
|
|
||||||
|
self.wrapped = T
|
||||||
|
|
||||||
|
@__init__.register
|
||||||
|
def __init__(self, T: gp_Trsf) -> None:
|
||||||
|
"""Location wrapping the low-level gp_Trsf object t"""
|
||||||
|
|
||||||
|
self.wrapped = TopLoc_Location(T)
|
||||||
|
|
||||||
|
@__init__.register
|
||||||
|
def __init__(self, t: VectorLike, ax: VectorLike, angle: Real) -> None:
|
||||||
|
"""Location with translation t and rotation around ax by angle
|
||||||
|
with respect to the original location."""
|
||||||
|
|
||||||
|
T = gp_Trsf()
|
||||||
|
T.SetRotation(gp_Ax1(Vector().toPnt(), Vector(ax).toDir()), radians(angle))
|
||||||
|
T.SetTranslationPart(Vector(t).wrapped)
|
||||||
|
|
||||||
|
self.wrapped = TopLoc_Location(T)
|
||||||
|
|
||||||
|
@__init__.register
|
||||||
|
def __init__(self, t: VectorLike, angles: Tuple[Real, Real, Real]) -> None:
|
||||||
|
"""Location with translation t and 3 rotation angles."""
|
||||||
|
|
||||||
|
T = gp_Trsf()
|
||||||
|
|
||||||
|
q = gp_Quaternion()
|
||||||
|
q.SetEulerAngles(gp_Extrinsic_XYZ, *map(radians, angles))
|
||||||
|
|
||||||
|
T.SetRotation(q)
|
||||||
|
T.SetTranslationPart(Vector(t).wrapped)
|
||||||
|
|
||||||
self.wrapped = TopLoc_Location(T)
|
self.wrapped = TopLoc_Location(T)
|
||||||
|
|
||||||
@ -1035,6 +1054,6 @@ class Location(object):
|
|||||||
rot = T.GetRotation()
|
rot = T.GetRotation()
|
||||||
|
|
||||||
rv_trans = (trans.X(), trans.Y(), trans.Z())
|
rv_trans = (trans.X(), trans.Y(), trans.Z())
|
||||||
rv_rot = rot.GetEulerAngles(gp_EulerSequence.gp_Extrinsic_XYZ)
|
rx, ry, rz = rot.GetEulerAngles(gp_EulerSequence.gp_Extrinsic_XYZ)
|
||||||
|
|
||||||
return rv_trans, rv_rot
|
return rv_trans, (degrees(rx), degrees(ry), degrees(rz))
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -24,7 +24,7 @@ requirements:
|
|||||||
- typing_extensions
|
- typing_extensions
|
||||||
- nptyping >=2.0.1
|
- nptyping >=2.0.1
|
||||||
- nlopt
|
- nlopt
|
||||||
- multimethod ==1.9.1
|
- multimethod >=1.11,<2.0
|
||||||
- casadi
|
- casadi
|
||||||
|
|
||||||
test:
|
test:
|
||||||
|
@ -84,6 +84,10 @@ Class Details
|
|||||||
:members:
|
:members:
|
||||||
:special-members:
|
:special-members:
|
||||||
|
|
||||||
|
.. automodule:: cadquery.occ_impl.shapes
|
||||||
|
:show-inheritance:
|
||||||
|
:members:
|
||||||
|
|
||||||
.. autoclass:: cadquery.occ_impl.shapes.Mixin1D
|
.. autoclass:: cadquery.occ_impl.shapes.Mixin1D
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
:members:
|
:members:
|
||||||
|
238
doc/free-func.rst
Normal file
238
doc/free-func.rst
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
.. _freefuncapi:
|
||||||
|
|
||||||
|
*****************
|
||||||
|
Free function API
|
||||||
|
*****************
|
||||||
|
|
||||||
|
.. warning:: The free function API is experimental and may change.
|
||||||
|
|
||||||
|
For situations when more freedom in crafting individual objects is required, a free function API is provided.
|
||||||
|
This API has no hidden state, but may result in more verbose code. One can still use selectors as methods, but all other operations are implemented as free functions.
|
||||||
|
Placement of objects and creation of patterns can be achieved using the various overloads of the moved method.
|
||||||
|
|
||||||
|
Currently this documentation is incomplete, more examples can be found in the tests.
|
||||||
|
|
||||||
|
Tutorial
|
||||||
|
--------
|
||||||
|
|
||||||
|
The purpose of this section is to demonstrate how to construct Shape objects using the free function API.
|
||||||
|
|
||||||
|
|
||||||
|
.. cadquery::
|
||||||
|
:height: 600px
|
||||||
|
|
||||||
|
from cadquery.occ_impl.shapes import *
|
||||||
|
|
||||||
|
dh = 2
|
||||||
|
r = 1
|
||||||
|
|
||||||
|
# construct edges
|
||||||
|
edge1 = circle(r)
|
||||||
|
edge2 = circle(1.5*r).moved(z=dh)
|
||||||
|
edge3 = circle(r).moved(z=1.5*dh)
|
||||||
|
|
||||||
|
# loft the side face
|
||||||
|
side = loft(edge1, edge2, edge3)
|
||||||
|
|
||||||
|
# bottom face
|
||||||
|
bottom = fill(side.edges('<Z'))
|
||||||
|
|
||||||
|
# top face with continuous curvature
|
||||||
|
top = cap(side.edges('>Z'), side, [(0,0,1.6*dh)])
|
||||||
|
|
||||||
|
# assemble into a solid
|
||||||
|
s = solid(side, bottom, top)
|
||||||
|
|
||||||
|
# construct the final result
|
||||||
|
result = s.moved((-3*r, 0, 0), (3*r, 0, 0))
|
||||||
|
|
||||||
|
|
||||||
|
The code above builds a non-trivial object by sequentially constructing individual faces, assembling them into a solid and finally generating a pattern.
|
||||||
|
|
||||||
|
It begins with defining few edges.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
edge1 = circle(r)
|
||||||
|
edge2 = circle(2*r).moved(z=dh)
|
||||||
|
edge3 = circle(r).moved(z=1.5*dh)
|
||||||
|
|
||||||
|
|
||||||
|
Those edges are used to create the side faces of the final solid using :meth:`~cadquery.occ_impl.shapes.loft`.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
side = loft(edge1, edge2, edge3)
|
||||||
|
|
||||||
|
Once the side is there, :meth:`~cadquery.occ_impl.shapes.cap` and :meth:`~cadquery.occ_impl.shapes.fill` are used to define the top and bottom faces.
|
||||||
|
Note that :meth:`~cadquery.occ_impl.shapes.cap` tries to maintain curvature continuity with respect to the context shape. This is not the case for :meth:`~cadquery.occ_impl.shapes.fill`.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# bottom face
|
||||||
|
bottom = fill(side.edges('<Z'))
|
||||||
|
|
||||||
|
# top face with continuous curvature
|
||||||
|
top = cap(side.edges('>Z'), side, [(0,0,1.75*dh)])
|
||||||
|
|
||||||
|
Next, all the faces are assembled into a solid.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
s = solid(side, bottom, top)
|
||||||
|
|
||||||
|
Finally, the solid is duplicated and placed in the desired locations creating the final compound object. Note various usages of :meth:`~cadquery.Shape.moved`.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
result = s.moved((-3*r, 0, 0), (3*r, 0, 0))
|
||||||
|
|
||||||
|
In general all the operations are implemented as free functions, with the exception of placement and selection which are strictly related to a specific shape.
|
||||||
|
|
||||||
|
|
||||||
|
Primitives
|
||||||
|
----------
|
||||||
|
|
||||||
|
Various 1D, 2D and 3D primitives are supported.
|
||||||
|
|
||||||
|
.. cadquery::
|
||||||
|
|
||||||
|
from cadquery.occ_impl.shapes import *
|
||||||
|
|
||||||
|
e = segment((0,0), (0,1))
|
||||||
|
|
||||||
|
c = circle(1)
|
||||||
|
|
||||||
|
f = plane(1, 1.5)
|
||||||
|
|
||||||
|
b = box(1, 1, 1)
|
||||||
|
|
||||||
|
result = compound(e, c.move(2), f.move(4), b.move(6))
|
||||||
|
|
||||||
|
|
||||||
|
Boolean operations
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Boolean operations are supported and implemented as operators and free functions.
|
||||||
|
In general boolean operations are slow and it is advised to avoid them and not to perform the in a loop.
|
||||||
|
One can for example union multiple solids at once by first combining them into a compound.
|
||||||
|
|
||||||
|
.. cadquery::
|
||||||
|
|
||||||
|
from cadquery.occ_impl.shapes import *
|
||||||
|
|
||||||
|
c1 = cylinder(1, 2)
|
||||||
|
c2 = cylinder(0.5, 3)
|
||||||
|
|
||||||
|
f1 = plane(2, 2).move(z=1)
|
||||||
|
f2 = plane(1, 1).move(z=1)
|
||||||
|
|
||||||
|
e1 = segment((0,-2.5, 1), (0,2.5,1))
|
||||||
|
|
||||||
|
# union
|
||||||
|
r1 = c2 + c1
|
||||||
|
r2 = fuse(f1, f2)
|
||||||
|
|
||||||
|
# difference
|
||||||
|
r3 = c1 - c2
|
||||||
|
r4 = cut(f1, f2)
|
||||||
|
|
||||||
|
# intersection
|
||||||
|
r5 = c1*c2
|
||||||
|
r6 = intersect(f1, f2)
|
||||||
|
|
||||||
|
# splitting
|
||||||
|
r7 = (c1 / f1).solids('<Z')
|
||||||
|
r8 = split(f2, e1).faces('<X')
|
||||||
|
|
||||||
|
results = (r1, r2, r3, r4, r5, r6, r7, r8)
|
||||||
|
result = compound([el.moved(2*i) for i,el in enumerate(results)])
|
||||||
|
|
||||||
|
Note that bool operations work on 2D shapes as well.
|
||||||
|
|
||||||
|
|
||||||
|
Shape construction
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Constructing complex shapes from simple shapes is possible in various contexts.
|
||||||
|
|
||||||
|
.. cadquery::
|
||||||
|
|
||||||
|
from cadquery.occ_impl.shapes import *
|
||||||
|
|
||||||
|
e1 = segment((0,0), (1,0))
|
||||||
|
e2 = segment((1,0), (1,1))
|
||||||
|
|
||||||
|
# wire from edges
|
||||||
|
r1 = wire(e1, e2)
|
||||||
|
|
||||||
|
c1 = circle(1)
|
||||||
|
|
||||||
|
# face from a planar wire
|
||||||
|
r2 = face(c1)
|
||||||
|
|
||||||
|
# solid from faces
|
||||||
|
f1 = plane(1,1)
|
||||||
|
f2 = f1.moved(z=1)
|
||||||
|
f3 = extrude(f1.wires(), (0,0,1))
|
||||||
|
|
||||||
|
r3 = solid(f1,f2,*f3)
|
||||||
|
|
||||||
|
# compound from shapes
|
||||||
|
s1 = circle(1).moved(ry=90)
|
||||||
|
s2 = plane(1,1).move(rx=90).move(y=2)
|
||||||
|
s3 = cone(1,1.5).move(y=4)
|
||||||
|
|
||||||
|
r4 = compound(s1, s2, s3)
|
||||||
|
|
||||||
|
results = (r1, r2, r3, r4,)
|
||||||
|
result = compound([el.moved(2*i) for i,el in enumerate(results)])
|
||||||
|
|
||||||
|
|
||||||
|
Operations
|
||||||
|
----------
|
||||||
|
|
||||||
|
Free function API currently supports :meth:`~cadquery.occ_impl.shapes.extrude`, :meth:`~cadquery.occ_impl.shapes.loft`, :meth:`~cadquery.occ_impl.shapes.revolve` and :meth:`~cadquery.occ_impl.shapes.sweep` operations.
|
||||||
|
|
||||||
|
.. cadquery::
|
||||||
|
|
||||||
|
from cadquery.occ_impl.shapes import *
|
||||||
|
|
||||||
|
r = rect(1,0.5)
|
||||||
|
c = circle(0.2)
|
||||||
|
p = spline([(0,0,0), (0,1,2)], [(0,0,1), (0,1,1)])
|
||||||
|
|
||||||
|
# extrude
|
||||||
|
s1 = extrude(r, (0,0,2))
|
||||||
|
s2 = extrude(fill(r), (0,0,1))
|
||||||
|
|
||||||
|
# sweep
|
||||||
|
s3 = sweep(r, p)
|
||||||
|
s4 = sweep(r, p, cap=True)
|
||||||
|
|
||||||
|
# loft
|
||||||
|
s5 = loft(r, c.moved(z=2))
|
||||||
|
s6 = loft(r, c.moved(z=1), cap=True)\
|
||||||
|
|
||||||
|
# revolve
|
||||||
|
s7 = revolve(fill(r), (0.5, 0, 0), (0, 1, 0), 90)
|
||||||
|
|
||||||
|
results = (s1, s2, s3, s4, s5, s6, s7)
|
||||||
|
result = compound([el.moved(2*i) for i,el in enumerate(results)])
|
||||||
|
|
||||||
|
|
||||||
|
Placement
|
||||||
|
---------
|
||||||
|
|
||||||
|
Placement and creation of arrays is possible using :meth:`~cadquery.Shape.move` and :meth:`~cadquery.Shape.moved`.
|
||||||
|
|
||||||
|
.. cadquery::
|
||||||
|
|
||||||
|
from cadquery.occ_impl.shapes import *
|
||||||
|
|
||||||
|
locs = [(0,-1,0), (0,1,0)]
|
||||||
|
|
||||||
|
s = sphere(1).moved(locs)
|
||||||
|
c = cylinder(1,2).move(rx=15).moved(*locs)
|
||||||
|
|
||||||
|
result = compound(s, c.moved(2))
|
@ -40,6 +40,7 @@ Table Of Contents
|
|||||||
primer.rst
|
primer.rst
|
||||||
sketch.rst
|
sketch.rst
|
||||||
assy.rst
|
assy.rst
|
||||||
|
free-func.rst
|
||||||
fileformat.rst
|
fileformat.rst
|
||||||
examples.rst
|
examples.rst
|
||||||
apireference.rst
|
apireference.rst
|
||||||
|
@ -211,7 +211,7 @@ Or like this : ::
|
|||||||
It's then more difficult to debug as you cannot visualize each operation step by step, which is a functionality that is provided by the CQ-Editor debugger for example.
|
It's then more difficult to debug as you cannot visualize each operation step by step, which is a functionality that is provided by the CQ-Editor debugger for example.
|
||||||
|
|
||||||
The Direct API
|
The Direct API
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
While the fluent API exposes much functionality, you may find scenarios that require extra flexibility or require working with lower level objects.
|
While the fluent API exposes much functionality, you may find scenarios that require extra flexibility or require working with lower level objects.
|
||||||
|
|
||||||
@ -229,7 +229,7 @@ The 9 topological classes are :
|
|||||||
8. :class:`~cadquery.Edge`
|
8. :class:`~cadquery.Edge`
|
||||||
9. :class:`~cadquery.Vertex`
|
9. :class:`~cadquery.Vertex`
|
||||||
|
|
||||||
Each class has its own methods to create and/or edit shapes of their respective type. As already explained in :ref:`cadquery_concepts` there is also some kind of hierarchy in the
|
Each class has its own methods to create and/or edit shapes of their respective type. One can also use the :ref:`freefuncapi` to create and modify shapes. As already explained in :ref:`cadquery_concepts` there is also some kind of hierarchy in the
|
||||||
topological classes. A Wire is made of several edges which are themselves made of several vertices. This means you can create geometry from the bottom up and have a lot of control over it.
|
topological classes. A Wire is made of several edges which are themselves made of several vertices. This means you can create geometry from the bottom up and have a lot of control over it.
|
||||||
|
|
||||||
For example we can create a circular face like so ::
|
For example we can create a circular face like so ::
|
||||||
@ -246,7 +246,7 @@ It is more verbose than the fluent API and more tedious to work with, but as it
|
|||||||
it is sometimes more convenient than the fluent API.
|
it is sometimes more convenient than the fluent API.
|
||||||
|
|
||||||
The OCCT API
|
The OCCT API
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
Finally we are discussing about the OCCT API. The OCCT API is the lowest level of CadQuery. The direct API is built upon the OCCT API, where the OCCT API in CadQuery is available through OCP.
|
Finally we are discussing about the OCCT API. The OCCT API is the lowest level of CadQuery. The direct API is built upon the OCCT API, where the OCCT API in CadQuery is available through OCP.
|
||||||
OCP are the Python bindings of the OCCT C++ libraries CadQuery uses. This means you have access to (almost) all the OCCT C++ libraries in Python and in CadQuery.
|
OCP are the Python bindings of the OCCT C++ libraries CadQuery uses. This means you have access to (almost) all the OCCT C++ libraries in Python and in CadQuery.
|
||||||
|
@ -19,7 +19,7 @@ dependencies:
|
|||||||
- nlopt
|
- nlopt
|
||||||
- path
|
- path
|
||||||
- casadi
|
- casadi
|
||||||
- multimethod ==1.9.1
|
- multimethod >=1.11,<2.0
|
||||||
- typed-ast
|
- typed-ast
|
||||||
- regex
|
- regex
|
||||||
- pathspec
|
- pathspec
|
||||||
|
1
mypy.ini
1
mypy.ini
@ -1,6 +1,7 @@
|
|||||||
[mypy]
|
[mypy]
|
||||||
ignore_missing_imports = False
|
ignore_missing_imports = False
|
||||||
disable_error_code = no-redef
|
disable_error_code = no-redef
|
||||||
|
plugins = mypy/cadquery-plugin.py
|
||||||
|
|
||||||
[mypy-ezdxf.*]
|
[mypy-ezdxf.*]
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
|
81
mypy/cadquery-plugin.py
Normal file
81
mypy/cadquery-plugin.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
from mypy.plugin import Plugin, FunctionContext
|
||||||
|
from mypy.types import Type, UnionType
|
||||||
|
|
||||||
|
|
||||||
|
class CadqueryPlugin(Plugin):
|
||||||
|
def get_function_hook(self, fullname: str):
|
||||||
|
|
||||||
|
if fullname == "cadquery.occ_impl.shapes._get":
|
||||||
|
|
||||||
|
return hook__get
|
||||||
|
|
||||||
|
elif fullname == "cadquery.occ_impl.shapes._get_one":
|
||||||
|
|
||||||
|
return hook__get_one
|
||||||
|
|
||||||
|
elif fullname == "cadquery.occ_impl.shapes._get_edges":
|
||||||
|
|
||||||
|
return hook__get_edges
|
||||||
|
|
||||||
|
elif fullname == "cadquery.occ_impl.shapes._get_wires":
|
||||||
|
|
||||||
|
return hook__get_wires
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def hook__get(ctx: FunctionContext) -> Type:
|
||||||
|
"""
|
||||||
|
Hook for cq.occ_impl.shapes._get
|
||||||
|
|
||||||
|
Based on the second argument values it adjusts return type to an Iterator of specific subclasses of Shape.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if hasattr(ctx.args[1][0], "items"):
|
||||||
|
return_type_names = [el.value for el in ctx.args[1][0].items]
|
||||||
|
else:
|
||||||
|
return_type_names = [ctx.args[1][0].value]
|
||||||
|
|
||||||
|
return_types = UnionType([ctx.api.named_type(n) for n in return_type_names])
|
||||||
|
|
||||||
|
return ctx.api.named_generic_type("typing.Iterable", [return_types])
|
||||||
|
|
||||||
|
|
||||||
|
def hook__get_one(ctx: FunctionContext) -> Type:
|
||||||
|
"""
|
||||||
|
Hook for cq.occ_impl.shapes._get_one
|
||||||
|
|
||||||
|
Based on the second argument values it adjusts return type to a Union of specific subclasses of Shape.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if hasattr(ctx.args[1][0], "items"):
|
||||||
|
return_type_names = [el.value for el in ctx.args[1][0].items]
|
||||||
|
else:
|
||||||
|
return_type_names = [ctx.args[1][0].value]
|
||||||
|
|
||||||
|
return UnionType([ctx.api.named_type(n) for n in return_type_names])
|
||||||
|
|
||||||
|
|
||||||
|
def hook__get_wires(ctx: FunctionContext) -> Type:
|
||||||
|
"""
|
||||||
|
Hook for cq.occ_impl.shapes._get_wires
|
||||||
|
"""
|
||||||
|
|
||||||
|
return_type = ctx.api.named_type("Wire")
|
||||||
|
|
||||||
|
return ctx.api.named_generic_type("typing.Iterable", [return_type])
|
||||||
|
|
||||||
|
|
||||||
|
def hook__get_edges(ctx: FunctionContext) -> Type:
|
||||||
|
"""
|
||||||
|
Hook for cq.occ_impl.shapes._get_edges
|
||||||
|
"""
|
||||||
|
|
||||||
|
return_type = ctx.api.named_type("Edge")
|
||||||
|
|
||||||
|
return ctx.api.named_generic_type("typing.Iterable", [return_type])
|
||||||
|
|
||||||
|
|
||||||
|
def plugin(version: str):
|
||||||
|
|
||||||
|
return CadqueryPlugin
|
2
setup.py
2
setup.py
@ -28,7 +28,7 @@ if not is_rtd and not is_appveyor and not is_azure and not is_conda:
|
|||||||
reqs = [
|
reqs = [
|
||||||
"cadquery-ocp>=7.7.0a0,<7.8",
|
"cadquery-ocp>=7.7.0a0,<7.8",
|
||||||
"ezdxf",
|
"ezdxf",
|
||||||
"multimethod==1.9.1",
|
"multimethod>=1.11,<2.0",
|
||||||
"nlopt",
|
"nlopt",
|
||||||
"nptyping==2.0.1",
|
"nptyping==2.0.1",
|
||||||
"typish",
|
"typish",
|
||||||
|
@ -616,6 +616,12 @@ class TestCadObjects(BaseTest):
|
|||||||
|
|
||||||
def testLocation(self):
|
def testLocation(self):
|
||||||
|
|
||||||
|
# empty
|
||||||
|
loc = Location()
|
||||||
|
|
||||||
|
T = loc.wrapped.Transformation().TranslationPart()
|
||||||
|
self.assertTupleAlmostEquals((T.X(), T.Y(), T.Z()), (0, 0, 0), 6)
|
||||||
|
|
||||||
# Tuple
|
# Tuple
|
||||||
loc0 = Location((0, 0, 1))
|
loc0 = Location((0, 0, 1))
|
||||||
|
|
||||||
@ -680,6 +686,14 @@ class TestCadObjects(BaseTest):
|
|||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
Location("xy_plane")
|
Location("xy_plane")
|
||||||
|
|
||||||
|
# test to tuple
|
||||||
|
loc8 = Location(z=2, ry=15)
|
||||||
|
|
||||||
|
trans, rot = loc8.toTuple()
|
||||||
|
|
||||||
|
self.assertTupleAlmostEquals(trans, (0, 0, 2), 6)
|
||||||
|
self.assertTupleAlmostEquals(rot, (0, 15, 0), 6)
|
||||||
|
|
||||||
def testEdgeWrapperRadius(self):
|
def testEdgeWrapperRadius(self):
|
||||||
|
|
||||||
# get a radius from a simple circle
|
# get a radius from a simple circle
|
||||||
|
531
tests/test_free_functions.py
Normal file
531
tests/test_free_functions.py
Normal file
@ -0,0 +1,531 @@
|
|||||||
|
from cadquery.occ_impl.shapes import (
|
||||||
|
vertex,
|
||||||
|
segment,
|
||||||
|
polyline,
|
||||||
|
polygon,
|
||||||
|
rect,
|
||||||
|
circle,
|
||||||
|
ellipse,
|
||||||
|
plane,
|
||||||
|
box,
|
||||||
|
cylinder,
|
||||||
|
sphere,
|
||||||
|
torus,
|
||||||
|
cone,
|
||||||
|
spline,
|
||||||
|
text,
|
||||||
|
clean,
|
||||||
|
fill,
|
||||||
|
cap,
|
||||||
|
extrude,
|
||||||
|
fillet,
|
||||||
|
chamfer,
|
||||||
|
revolve,
|
||||||
|
offset,
|
||||||
|
loft,
|
||||||
|
sweep,
|
||||||
|
cut,
|
||||||
|
fuse,
|
||||||
|
intersect,
|
||||||
|
wire,
|
||||||
|
face,
|
||||||
|
shell,
|
||||||
|
solid,
|
||||||
|
compound,
|
||||||
|
Location,
|
||||||
|
Shape,
|
||||||
|
_get_one_wire,
|
||||||
|
_get_wires,
|
||||||
|
_get,
|
||||||
|
_get_one,
|
||||||
|
_get_edges,
|
||||||
|
)
|
||||||
|
|
||||||
|
from pytest import approx, raises
|
||||||
|
from math import pi
|
||||||
|
|
||||||
|
#%% test utils
|
||||||
|
|
||||||
|
|
||||||
|
def assert_all_valid(*objs: Shape):
|
||||||
|
|
||||||
|
for o in objs:
|
||||||
|
assert o.isValid()
|
||||||
|
|
||||||
|
|
||||||
|
def vector_equal(v1, v2):
|
||||||
|
|
||||||
|
return v1.toTuple() == approx(v2.toTuple())
|
||||||
|
|
||||||
|
|
||||||
|
#%% utils
|
||||||
|
|
||||||
|
|
||||||
|
def test_utils():
|
||||||
|
|
||||||
|
r1 = _get_one_wire(rect(1, 1))
|
||||||
|
|
||||||
|
assert r1.ShapeType() == "Wire"
|
||||||
|
|
||||||
|
r2 = list(_get_wires(compound(r1, r1.moved(Location(0, 0, 1)))))
|
||||||
|
|
||||||
|
assert len(r2) == 2
|
||||||
|
assert all(el.ShapeType() == "Wire" for el in r2)
|
||||||
|
|
||||||
|
with raises(ValueError):
|
||||||
|
list(_get_wires(box(1, 1, 1)))
|
||||||
|
|
||||||
|
r3 = list(_get(box(1, 1, 1).moved(Location(), Location(2, 0, 0)), "Solid"))
|
||||||
|
|
||||||
|
assert (len(r3)) == 2
|
||||||
|
assert all(el.ShapeType() == "Solid" for el in r3)
|
||||||
|
|
||||||
|
with raises(ValueError):
|
||||||
|
list(_get(box(1, 1, 1), "Shell"))
|
||||||
|
|
||||||
|
r4 = _get_one(compound(box(1, 1, 1), box(2, 2, 2)), "Solid")
|
||||||
|
|
||||||
|
assert r4.ShapeType() == "Solid"
|
||||||
|
|
||||||
|
with raises(ValueError):
|
||||||
|
_get_one(rect(1, 1), ("Solid", "Shell"))
|
||||||
|
|
||||||
|
with raises(ValueError):
|
||||||
|
list(_get_edges(fill(circle(1))))
|
||||||
|
|
||||||
|
|
||||||
|
#%% constructors
|
||||||
|
|
||||||
|
|
||||||
|
def test_constructors():
|
||||||
|
|
||||||
|
# wire
|
||||||
|
e1 = segment((0, 0), (0, 1))
|
||||||
|
e2 = segment((0, 1), (1, 1))
|
||||||
|
e3 = segment((1, 1), (1, 0))
|
||||||
|
e4 = segment((1, 0), (0, 0))
|
||||||
|
|
||||||
|
w1 = wire(e1, e2, e3, e4)
|
||||||
|
w2 = wire((e1, e2, e3, e4))
|
||||||
|
|
||||||
|
assert w1.Length() == approx(4)
|
||||||
|
assert w2.Length() == approx(4)
|
||||||
|
|
||||||
|
# face
|
||||||
|
f1 = face(w1, circle(0.1).moved(Location(0.5, 0.5, 0)))
|
||||||
|
f2 = face((w1,))
|
||||||
|
|
||||||
|
assert f1.Area() < 1
|
||||||
|
assert len(f1.Wires()) == 2
|
||||||
|
assert f2.Area() == approx(1)
|
||||||
|
assert len(f2.Wires()) == 1
|
||||||
|
|
||||||
|
with raises(ValueError):
|
||||||
|
face(e1)
|
||||||
|
|
||||||
|
# shell
|
||||||
|
b = box(1, 1, 1)
|
||||||
|
|
||||||
|
sh1 = shell(b.Faces())
|
||||||
|
sh2 = shell(*b.Faces())
|
||||||
|
|
||||||
|
assert sh1.Area() == approx(6)
|
||||||
|
assert sh2.Area() == approx(6)
|
||||||
|
|
||||||
|
# solid
|
||||||
|
s1 = solid(b.Faces())
|
||||||
|
s2 = solid(*b.Faces())
|
||||||
|
|
||||||
|
assert s1.Volume() == approx(1)
|
||||||
|
assert s2.Volume() == approx(1)
|
||||||
|
|
||||||
|
# compound
|
||||||
|
c1 = compound(b.Faces())
|
||||||
|
c2 = compound(*b.Faces())
|
||||||
|
|
||||||
|
assert len(list(c1)) == 6
|
||||||
|
assert len(list(c2)) == 6
|
||||||
|
|
||||||
|
for f in list(c1) + list(c2):
|
||||||
|
assert f.ShapeType() == "Face"
|
||||||
|
|
||||||
|
|
||||||
|
#%% primitives
|
||||||
|
|
||||||
|
|
||||||
|
def test_vertex():
|
||||||
|
|
||||||
|
v = vertex((1, 2,))
|
||||||
|
|
||||||
|
assert v.isValid()
|
||||||
|
assert v.Center().toTuple() == approx((1, 2, 0))
|
||||||
|
|
||||||
|
v = vertex(1, 2, 3)
|
||||||
|
|
||||||
|
assert v.isValid()
|
||||||
|
assert v.Center().toTuple() == approx((1, 2, 3))
|
||||||
|
|
||||||
|
|
||||||
|
def test_segment():
|
||||||
|
|
||||||
|
s = segment((0, 0, 0), (0, 0, 1))
|
||||||
|
|
||||||
|
assert s.isValid()
|
||||||
|
assert s.Length() == approx(1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_polyline():
|
||||||
|
|
||||||
|
s = polyline((0, 0), (0, 1), (1, 1))
|
||||||
|
|
||||||
|
assert s.isValid()
|
||||||
|
assert s.Length() == approx(2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_polygon():
|
||||||
|
|
||||||
|
s = polygon((0, 0), (0, 1), (1, 1), (1, 0))
|
||||||
|
|
||||||
|
assert s.isValid()
|
||||||
|
assert s.IsClosed()
|
||||||
|
assert s.Length() == approx(4)
|
||||||
|
|
||||||
|
|
||||||
|
def test_rect():
|
||||||
|
|
||||||
|
s = rect(2, 1)
|
||||||
|
|
||||||
|
assert s.isValid()
|
||||||
|
assert s.IsClosed()
|
||||||
|
assert s.Length() == approx(6)
|
||||||
|
|
||||||
|
|
||||||
|
def test_circle():
|
||||||
|
|
||||||
|
s = circle(1)
|
||||||
|
|
||||||
|
assert s.isValid()
|
||||||
|
assert s.IsClosed()
|
||||||
|
assert s.Length() == approx(2 * pi)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ellipse():
|
||||||
|
|
||||||
|
s = ellipse(3, 2)
|
||||||
|
|
||||||
|
assert s.isValid()
|
||||||
|
assert s.IsClosed()
|
||||||
|
assert face(s).Area() == approx(6 * pi)
|
||||||
|
|
||||||
|
|
||||||
|
def test_plane():
|
||||||
|
|
||||||
|
s = plane(1, 2)
|
||||||
|
|
||||||
|
assert s.isValid()
|
||||||
|
assert s.Area() == approx(2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_box():
|
||||||
|
|
||||||
|
s = box(1, 1, 1)
|
||||||
|
|
||||||
|
assert s.isValid()
|
||||||
|
assert s.Volume() == approx(1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cylinder():
|
||||||
|
|
||||||
|
s = cylinder(2, 1)
|
||||||
|
|
||||||
|
assert s.isValid()
|
||||||
|
assert s.Volume() == approx(pi)
|
||||||
|
|
||||||
|
|
||||||
|
def test_sphere():
|
||||||
|
|
||||||
|
s = sphere(2)
|
||||||
|
|
||||||
|
assert s.isValid()
|
||||||
|
assert s.Volume() == approx(4 / 3 * pi)
|
||||||
|
|
||||||
|
|
||||||
|
def test_torus():
|
||||||
|
|
||||||
|
s = torus(10, 2)
|
||||||
|
|
||||||
|
assert s.isValid()
|
||||||
|
assert s.Volume() == approx(2 * pi ** 2 * 5)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cone():
|
||||||
|
|
||||||
|
s = cone(2, 1)
|
||||||
|
|
||||||
|
assert s.isValid()
|
||||||
|
assert s.Volume() == approx(1 / 3 * pi)
|
||||||
|
|
||||||
|
s = cone(2, 1, 1)
|
||||||
|
|
||||||
|
assert s.isValid()
|
||||||
|
assert s.Volume() == approx(1 / 3 * pi * (1 + 0.25 + 0.5))
|
||||||
|
|
||||||
|
|
||||||
|
def test_spline():
|
||||||
|
|
||||||
|
s1 = spline((0, 0), (0, 1))
|
||||||
|
s2 = spline([(0, 0), (0, 1)])
|
||||||
|
s3 = spline([(0, 0), (0, 1)], [(1, 0), (-1, 0)])
|
||||||
|
|
||||||
|
assert s1.Length() == approx(1)
|
||||||
|
assert s2.Length() == approx(1)
|
||||||
|
assert s3.Length() > 0
|
||||||
|
assert s3.tangentAt(0).toTuple() == approx((1, 0, 0))
|
||||||
|
assert s3.tangentAt(1).toTuple() == approx((-1, 0, 0))
|
||||||
|
|
||||||
|
|
||||||
|
def test_text():
|
||||||
|
|
||||||
|
r1 = text("CQ", 10)
|
||||||
|
|
||||||
|
assert len(r1.Faces()) == 2
|
||||||
|
assert len(r1.Wires()) == 3
|
||||||
|
assert r1.Area() > 0.0
|
||||||
|
|
||||||
|
# test alignemnt
|
||||||
|
r2 = text("CQ", 10, halign="left")
|
||||||
|
r3 = text("CQ", 10, halign="right")
|
||||||
|
r4 = text("CQ", 10, valign="bottom")
|
||||||
|
r5 = text("CQ", 10, valign="top")
|
||||||
|
|
||||||
|
assert r2.faces("<X").Center().x > r1.faces("<X").Center().x
|
||||||
|
assert r1.faces("<X").Center().x > r3.faces("<X").Center().x
|
||||||
|
assert r4.faces("<X").Center().y > r1.faces("<X").Center().y
|
||||||
|
assert r1.faces("<X").Center().y > r5.faces("<X").Center().x
|
||||||
|
|
||||||
|
|
||||||
|
#%% bool ops
|
||||||
|
def test_operators():
|
||||||
|
|
||||||
|
b1 = box(1, 1, 1).moved(Location(-0.5, -0.5, -0.5)) # small box
|
||||||
|
b2 = box(2, 2, 2).moved(Location(-1, -1, -1)) # large box
|
||||||
|
b3 = b1.moved(Location(0, 0, 1e-4)) # almost b1
|
||||||
|
f = plane(3, 3) # face
|
||||||
|
e = segment((-2, 0), (2, 0)) # edge
|
||||||
|
|
||||||
|
assert (b2 - b1).Volume() == approx(8 - 1)
|
||||||
|
|
||||||
|
assert (b2 * b1).Volume() == approx(1)
|
||||||
|
assert (b1 * f).Area() == approx(1)
|
||||||
|
assert (b1 * e).Length() == approx(1)
|
||||||
|
assert (f * e).Length() == approx(3)
|
||||||
|
|
||||||
|
assert (b2 + b1).Volume() == approx(8)
|
||||||
|
|
||||||
|
assert len((b1 / f).Solids()) == 2
|
||||||
|
|
||||||
|
# test fuzzy ops
|
||||||
|
assert len((b1 + b3).Faces()) == 14
|
||||||
|
assert (b1 - b3).Volume() > 0
|
||||||
|
assert (b1 * b3).Volume() < 1
|
||||||
|
|
||||||
|
assert len(fuse(b1, b3, 1e-3).Faces()) == 6
|
||||||
|
assert len(cut(b1, b3, 1e-3).Faces()) == 0
|
||||||
|
assert len(intersect(b1, b3, 1e-3).Faces()) == 6
|
||||||
|
|
||||||
|
|
||||||
|
#%% moved
|
||||||
|
def test_moved():
|
||||||
|
|
||||||
|
b = box(1, 1, 1)
|
||||||
|
l1 = Location((-1, 0, 0))
|
||||||
|
l2 = Location((1, 0, 0))
|
||||||
|
l3 = Location((0, 1, 0), (45, 0, 0))
|
||||||
|
l4 = Location((0, -1, 0), (-45, 0, 0))
|
||||||
|
|
||||||
|
bs1 = b.moved(l1, l2)
|
||||||
|
bs2 = b.moved((l1, l2))
|
||||||
|
|
||||||
|
assert bs1.Volume() == approx(2)
|
||||||
|
assert len(bs1.Solids()) == 2
|
||||||
|
|
||||||
|
assert bs2.Volume() == approx(2)
|
||||||
|
assert len(bs2.Solids()) == 2
|
||||||
|
|
||||||
|
# nested move
|
||||||
|
bs3 = bs1.moved(l3, l4)
|
||||||
|
|
||||||
|
assert bs3.Volume() == approx(4)
|
||||||
|
assert len(bs3.Solids()) == 4
|
||||||
|
|
||||||
|
# move with VectorLike
|
||||||
|
bs4 = b.moved((0, 0, 1), (0, 0, -1))
|
||||||
|
bs5 = bs4.moved((1, 0, 0)).move((-1, 0, 0))
|
||||||
|
|
||||||
|
assert bs4.Volume() == approx(2)
|
||||||
|
assert vector_equal(bs5.Center(), bs4.Center())
|
||||||
|
|
||||||
|
# move with direct params
|
||||||
|
bs6 = b.moved((0, 0, 1)).moved(0, 0, -1)
|
||||||
|
bs7 = b.moved((0, 0, 1)).moved(z=-1)
|
||||||
|
bs8 = b.moved(Location((0, 0, 0), (-45, 0, 0))).moved(rx=45)
|
||||||
|
bs9 = b.moved().move(Location((0, 0, 0), (-45, 0, 0))).move(rx=45)
|
||||||
|
|
||||||
|
assert vector_equal(bs6.Center(), b.Center())
|
||||||
|
assert vector_equal(bs7.Center(), b.Center())
|
||||||
|
assert vector_equal(bs8.edges(">Z").Center(), b.edges(">Z").Center())
|
||||||
|
assert vector_equal(bs9.edges(">Z").Center(), b.edges(">Z").Center())
|
||||||
|
|
||||||
|
|
||||||
|
#%% ops
|
||||||
|
def test_clean():
|
||||||
|
|
||||||
|
b1 = box(1, 1, 1)
|
||||||
|
b2 = b1.moved(Location(1, 0, 0))
|
||||||
|
|
||||||
|
len((b1 + b2).Faces()) == 10
|
||||||
|
len(clean(b1 + b2).Faces()) == 6
|
||||||
|
|
||||||
|
|
||||||
|
def test_fill():
|
||||||
|
|
||||||
|
w1 = rect(1, 1)
|
||||||
|
w2 = rect(0.5, 0.5).moved(Location(0, 0, 1))
|
||||||
|
|
||||||
|
f1 = fill(w1)
|
||||||
|
f2 = fill(w1, [(0, 0, 1)])
|
||||||
|
f3 = fill(w1, [w2])
|
||||||
|
|
||||||
|
assert f1.isValid()
|
||||||
|
assert f1.Area() == approx(1)
|
||||||
|
|
||||||
|
assert f2.isValid()
|
||||||
|
assert f2.Area() > 1
|
||||||
|
|
||||||
|
assert f3.isValid()
|
||||||
|
assert f3.Area() > 1
|
||||||
|
assert len(f3.Edges()) == 4
|
||||||
|
assert len(f3.Wires()) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_cap():
|
||||||
|
|
||||||
|
s = extrude(circle(1), (0, 0, 1))
|
||||||
|
|
||||||
|
f1 = cap(s.edges(">Z"), s, [(0, 0, 1.5)])
|
||||||
|
f2 = cap(s.edges(">Z"), s, [circle(0.5).moved(Location(0, 0, 2))])
|
||||||
|
|
||||||
|
assert_all_valid(f1, f2)
|
||||||
|
assert f1.Area() > pi
|
||||||
|
assert f2.Area() > pi
|
||||||
|
|
||||||
|
|
||||||
|
def test_fillet():
|
||||||
|
|
||||||
|
b = box(1, 1, 1)
|
||||||
|
|
||||||
|
r = fillet(b, b.edges(">Z"), 0.1)
|
||||||
|
|
||||||
|
assert r.isValid()
|
||||||
|
assert len(r.Edges()) == 20
|
||||||
|
assert r.faces(">Z").Area() < 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_chamfer():
|
||||||
|
|
||||||
|
b = box(1, 1, 1)
|
||||||
|
|
||||||
|
r = chamfer(b, b.edges(">Z"), 0.1)
|
||||||
|
|
||||||
|
assert r.isValid()
|
||||||
|
assert len(r.Edges()) == 20
|
||||||
|
assert r.faces(">Z").Area() < 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_extrude():
|
||||||
|
|
||||||
|
v = vertex(0, 0, 0)
|
||||||
|
e = segment((0, 0), (0, 1))
|
||||||
|
w = rect(1, 1)
|
||||||
|
f = fill(w)
|
||||||
|
|
||||||
|
d = (0, 0, 1)
|
||||||
|
|
||||||
|
r1 = extrude(v, d)
|
||||||
|
r2 = extrude(e, d)
|
||||||
|
r3 = extrude(w, d)
|
||||||
|
r4 = extrude(f, d)
|
||||||
|
|
||||||
|
assert r1.Length() == approx(1)
|
||||||
|
assert r2.Area() == approx(1)
|
||||||
|
assert r3.Area() == approx(4)
|
||||||
|
assert r4.Volume() == approx(1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_revolve():
|
||||||
|
|
||||||
|
w = rect(1, 1)
|
||||||
|
|
||||||
|
r = revolve(w, (0.5, 0, 0), (0, 1, 0))
|
||||||
|
|
||||||
|
assert r.Volume() == approx(4 * pi)
|
||||||
|
|
||||||
|
|
||||||
|
def test_offset():
|
||||||
|
|
||||||
|
f = plane(1, 1)
|
||||||
|
s = box(1, 1, 1).shells()
|
||||||
|
|
||||||
|
r1 = offset(f, 1)
|
||||||
|
r2 = offset(s, -0.25)
|
||||||
|
|
||||||
|
assert r1.Volume() == approx(1)
|
||||||
|
assert r2.Volume() == approx(1 - 0.5 ** 3)
|
||||||
|
|
||||||
|
|
||||||
|
def test_sweep():
|
||||||
|
|
||||||
|
w1 = rect(1, 1)
|
||||||
|
w2 = w1.moved(Location(0, 0, 1))
|
||||||
|
|
||||||
|
p1 = segment((0, 0, 0), (0, 0, 1))
|
||||||
|
p2 = spline((w1.Center(), w2.Center()), ((-0.5, 0, 1), (0.5, 0, 1)))
|
||||||
|
|
||||||
|
r1 = sweep(w1, p1)
|
||||||
|
r2 = sweep((w1, w2), p1)
|
||||||
|
r3 = sweep(w1, p1, cap=True)
|
||||||
|
r4 = sweep((w1, w2), p1, cap=True)
|
||||||
|
r5 = sweep((w1, w2), p2, cap=True)
|
||||||
|
|
||||||
|
assert_all_valid(r1, r2, r3, r4, r5)
|
||||||
|
|
||||||
|
assert r1.Area() == approx(4)
|
||||||
|
assert r2.Area() == approx(4)
|
||||||
|
assert r3.Volume() == approx(1)
|
||||||
|
assert r4.Volume() == approx(1)
|
||||||
|
assert r5.Volume() > 0
|
||||||
|
assert len(r5.Faces()) == 6
|
||||||
|
|
||||||
|
|
||||||
|
def test_loft():
|
||||||
|
|
||||||
|
w1 = circle(1)
|
||||||
|
w2 = ellipse(1.5, 1).move(0, y=1)
|
||||||
|
w3 = circle(1).moved(z=4, rx=15)
|
||||||
|
|
||||||
|
w4 = segment((0, 0), (1, 0))
|
||||||
|
w5 = w4.moved(0, 0, 1)
|
||||||
|
|
||||||
|
r1 = loft(w1, w2, w3) # loft
|
||||||
|
r2 = loft(w1, w2, w3, ruled=True) # ruled loft
|
||||||
|
r3 = loft([w1, w2, w3]) # overload
|
||||||
|
r4 = loft(w1, w2, w3, cap=True) # capped loft
|
||||||
|
r5 = loft(w4, w5) # loft with open edges
|
||||||
|
|
||||||
|
assert_all_valid(r1, r2, r3, r4, r5)
|
||||||
|
|
||||||
|
assert len(r1.Faces()) == 1
|
||||||
|
assert len(r2.Faces()) == 2
|
||||||
|
assert len((r1 - r3).Faces()) == 0
|
||||||
|
assert r4.Volume() > 0
|
||||||
|
assert r5.Area() == approx(1)
|
Reference in New Issue
Block a user