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:
AU
2024-05-05 11:25:45 +02:00
committed by GitHub
parent 552203713b
commit 995d1393bd
13 changed files with 1952 additions and 74 deletions

View File

@ -1,4 +1,4 @@
import math
from math import pi, radians, degrees
from typing import overload, Sequence, Union, Tuple, Type, Optional
@ -14,6 +14,8 @@ from OCP.gp import (
gp_XYZ,
gp_EulerSequence,
gp,
gp_Quaternion,
gp_Extrinsic_XYZ,
)
from OCP.Bnd import Bnd_Box
from OCP.BRepBndLib import BRepBndLib
@ -22,6 +24,7 @@ from OCP.TopoDS import TopoDS_Shape
from OCP.TopLoc import TopLoc_Location
from ..types import Real
from ..utils import multimethod
TOL = 1e-2
@ -682,7 +685,7 @@ class Plane(object):
# NB: this is not a geometric Vector
rotate = Vector(rotate)
# Convert to radians.
rotate = rotate.multiply(math.pi / 180.0)
rotate = rotate.multiply(pi / 180.0)
# Compute rotation matrix.
T1 = gp_Trsf()
@ -942,74 +945,90 @@ class Location(object):
wrapped: TopLoc_Location
@overload
def __init__(self) -> None:
"""Empty location with not rotation or translation with respect to the original location."""
...
@overload
@multimethod
def __init__(self, t: VectorLike) -> None:
"""Location with translation t with respect to the original location."""
...
@overload
def __init__(self, t: Plane) -> None:
"""Location corresponding to the location of the Plane t."""
...
T = gp_Trsf()
T.SetTranslationPart(Vector(t).wrapped)
@overload
def __init__(self, t: Plane, v: VectorLike) -> None:
"""Location corresponding to the angular location of the Plane t with translation v."""
...
self.wrapped = TopLoc_Location(T)
@overload
def __init__(self, t: TopLoc_Location) -> None:
"""Location wrapping the low-level TopLoc_Location object t"""
...
@overload
def __init__(self, t: gp_Trsf) -> None:
"""Location wrapping the low-level gp_Trsf object t"""
...
@overload
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):
@__init__.register
def __init__(
self,
x: Real = 0,
y: Real = 0,
z: Real = 0,
rx: Real = 0,
ry: Real = 0,
rz: Real = 0,
) -> None:
"""Location with translation (x,y,z) and 3 rotation angles."""
T = gp_Trsf()
if len(args) == 0:
pass
elif len(args) == 1:
t = args[0]
q = gp_Quaternion()
q.SetEulerAngles(gp_Extrinsic_XYZ, radians(rx), radians(ry), radians(rz))
if isinstance(t, (Vector, tuple)):
T.SetRotation(q)
T.SetTranslationPart(Vector(x, y, z).wrapped)
self.wrapped = TopLoc_Location(T)
@__init__.register
def __init__(self, t: Plane) -> None:
"""Location corresponding to the location of the Plane t."""
T = gp_Trsf()
T.SetTransformation(gp_Ax3(t.origin.toPnt(), t.zDir.toDir(), t.xDir.toDir()))
T.Invert()
self.wrapped = TopLoc_Location(T)
@__init__.register
def __init__(self, t: Plane, v: VectorLike) -> None:
"""Location corresponding to the angular location of the Plane t with translation v."""
T = gp_Trsf()
T.SetTransformation(gp_Ax3(Vector(v).toPnt(), t.zDir.toDir(), t.xDir.toDir()))
T.Invert()
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)
elif isinstance(t, Plane):
cs = gp_Ax3(t.origin.toPnt(), t.zDir.toDir(), t.xDir.toDir())
T.SetTransformation(cs)
T.Invert()
elif isinstance(t, TopLoc_Location):
self.wrapped = t
return
elif isinstance(t, gp_Trsf):
T = t
else:
raise TypeError("Unexpected parameters")
elif len(args) == 2:
t, v = args
cs = gp_Ax3(Vector(v).toPnt(), t.zDir.toDir(), t.xDir.toDir())
T.SetTransformation(cs)
T.Invert()
else:
t, ax, angle = args
T.SetRotation(
gp_Ax1(Vector().toPnt(), Vector(ax).toDir()), angle * math.pi / 180.0
)
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)
@ -1035,6 +1054,6 @@ class Location(object):
rot = T.GetRotation()
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

View File

@ -24,7 +24,7 @@ requirements:
- typing_extensions
- nptyping >=2.0.1
- nlopt
- multimethod ==1.9.1
- multimethod >=1.11,<2.0
- casadi
test:

View File

@ -84,6 +84,10 @@ Class Details
:members:
:special-members:
.. automodule:: cadquery.occ_impl.shapes
:show-inheritance:
:members:
.. autoclass:: cadquery.occ_impl.shapes.Mixin1D
:show-inheritance:
:members:

238
doc/free-func.rst Normal file
View 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))

View File

@ -40,6 +40,7 @@ Table Of Contents
primer.rst
sketch.rst
assy.rst
free-func.rst
fileformat.rst
examples.rst
apireference.rst

View File

@ -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.
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.
@ -229,7 +229,7 @@ The 9 topological classes are :
8. :class:`~cadquery.Edge`
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.
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.
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.
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.

View File

@ -19,7 +19,7 @@ dependencies:
- nlopt
- path
- casadi
- multimethod ==1.9.1
- multimethod >=1.11,<2.0
- typed-ast
- regex
- pathspec

View File

@ -1,6 +1,7 @@
[mypy]
ignore_missing_imports = False
disable_error_code = no-redef
plugins = mypy/cadquery-plugin.py
[mypy-ezdxf.*]
ignore_missing_imports = True

81
mypy/cadquery-plugin.py Normal file
View 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

View File

@ -28,7 +28,7 @@ if not is_rtd and not is_appveyor and not is_azure and not is_conda:
reqs = [
"cadquery-ocp>=7.7.0a0,<7.8",
"ezdxf",
"multimethod==1.9.1",
"multimethod>=1.11,<2.0",
"nlopt",
"nptyping==2.0.1",
"typish",

View File

@ -616,6 +616,12 @@ class TestCadObjects(BaseTest):
def testLocation(self):
# empty
loc = Location()
T = loc.wrapped.Transformation().TranslationPart()
self.assertTupleAlmostEquals((T.X(), T.Y(), T.Z()), (0, 0, 0), 6)
# Tuple
loc0 = Location((0, 0, 1))
@ -680,6 +686,14 @@ class TestCadObjects(BaseTest):
with self.assertRaises(TypeError):
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):
# get a radius from a simple circle

View 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)