mirror of
https://github.com/gumyr/build123d.git
synced 2025-12-06 02:30:55 -08:00
Removed Workplanes
This commit is contained in:
parent
205a7b2aea
commit
c52ccd9174
9 changed files with 128 additions and 151 deletions
|
|
@ -32,14 +32,6 @@ Enums
|
|||
.. autoclass:: Transition
|
||||
.. autoclass:: Until
|
||||
|
||||
**********
|
||||
Workplanes
|
||||
**********
|
||||
|
||||
.. py:module:: build_common
|
||||
|
||||
.. autoclass:: Workplanes
|
||||
|
||||
*********
|
||||
Locations
|
||||
*********
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ Cheat Sheet
|
|||
|
||||
| :class:`~build_line.BuildLine` :class:`~build_part.BuildPart` :class:`~build_sketch.BuildSketch`
|
||||
| :class:`~build_common.GridLocations` :class:`~build_common.HexLocations` :class:`~build_common.Locations` :class:`~build_common.PolarLocations`
|
||||
| :class:`~build_common.Workplanes`
|
||||
|
||||
.. card:: Objects
|
||||
|
||||
|
|
|
|||
|
|
@ -271,7 +271,7 @@ provide multiple output formats, support for multiple languages and can be
|
|||
integrated with code management tools.
|
||||
|
||||
****************************************
|
||||
Key Concepts (context mode)
|
||||
Key Concepts (builder mode)
|
||||
****************************************
|
||||
|
||||
.. include:: key_concepts.rst
|
||||
|
|
|
|||
|
|
@ -1,3 +1,11 @@
|
|||
There are two primary APIs provided by build123d: builder and algebra. The builder
|
||||
api may be easier for new users as it provides some assistance and shortcuts; however,
|
||||
if you know what a Quaternion is you might prefer the algebra API which allows
|
||||
CAD objects to be created in the style of mathematical equations. Both API can
|
||||
be mixed in the same model with the exception that the algebra API can't be used
|
||||
from within a builder context. As with music, there is no "best" genre or API,
|
||||
use the one you prefer or both if you like.
|
||||
|
||||
The following key concepts will help new users understand build123d quickly.
|
||||
|
||||
Builders
|
||||
|
|
@ -59,39 +67,36 @@ information - as follows:
|
|||
In this example, ``Box`` is in the scope of ``part_builder`` while ``Circle``
|
||||
is in the scope of ``sketch_builder``.
|
||||
|
||||
Workplane Contexts
|
||||
==================
|
||||
Workplanes
|
||||
==========
|
||||
|
||||
As build123d is a 3D CAD package one must be able to position objects anywhere. As one
|
||||
frequently will work in the same plane for a sequence of operations, a workplane is used
|
||||
to aid in the location of features. The default workplane in most cases is the XY plane
|
||||
frequently will work in the same plane for a sequence of operations, the first parameter(s)
|
||||
of the builders is a (sequence of) workplane(s) which is (are) used
|
||||
to aid in the location of features. The default workplane in most cases is the ``Plane.XY``
|
||||
where a tuple of numbers represent positions on the x and y axes. However workplanes can
|
||||
be generated on any plane which allows users to put a workplane where they are working
|
||||
and then work in local 2D coordinate space.
|
||||
|
||||
To facilitate this a ``Workplanes`` stateful context is used to create a scope where a given
|
||||
workplane will apply. For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
with BuildPart(Plane.XY) as example:
|
||||
with BuildSketch() as bottom:
|
||||
with BuildSketch(example.faces().sort_by(sort_by=Axis.Z)[0]) as bottom:
|
||||
...
|
||||
with Workplanes(Plane.XZ) as vertical:
|
||||
with BuildSketch() as side:
|
||||
with BuildSketch(Plane.XZ) as vertical:
|
||||
...
|
||||
with Workplanes(example.faces().sort_by(SortBy.Z)[-1]):
|
||||
with BuildSketch() as top:
|
||||
with BuildSketch(example.faces().sort_by(sort_by=Axis.Z)[-1]) as top:
|
||||
...
|
||||
|
||||
When ``BuildPart`` is invoked it creates the workplane provided as a parameter (which has a
|
||||
default of the XY plane). The ``bottom`` sketch is therefore created on the XY plane. Subsequently
|
||||
the user has created the ``vertical`` plane (XZ) on which the ``side`` sketch is created. All
|
||||
objects or operations within the scope of a workplane will automatically be orientated with
|
||||
default of the ``Plane.XY``). The ``bottom`` sketch is therefore created on the ``Plane.XY`` but with the
|
||||
normal reversed to point down. Subsequently the user has created the ``vertical`` (``Plane.XZ```) sketch.
|
||||
All objects or operations within the scope of a workplane will automatically be orientated with
|
||||
respect to this plane so the user only has to work with local coordinates.
|
||||
|
||||
Workplanes can be created from faces as well. The ``top`` sketch is positioned on top
|
||||
of ``example`` by selecting its faces and finding the one with the greatest z value.
|
||||
As shown above, workplanes can be created from faces as well. The ``top`` sketch is
|
||||
positioned on top of ``example`` by selecting its faces and finding the one with the greatest z value.
|
||||
|
||||
One is not limited to a single workplane at a time. In the following example all six
|
||||
faces of the first box is used to define workplanes which are then used to position
|
||||
|
|
@ -103,8 +108,9 @@ rotated boxes.
|
|||
|
||||
with bd.BuildPart() as bp:
|
||||
bd.Box(3, 3, 3)
|
||||
with bd.Workplanes(*bp.faces()):
|
||||
bd.Box(1, 2, 0.1, rotation=(0, 0, 45))
|
||||
with bd.BuildSketch(*bp.faces()):
|
||||
bd.Rectangle(1, 2, rotation=45)
|
||||
bd.extrude(amount=0.1)
|
||||
|
||||
This is the result:
|
||||
|
||||
|
|
@ -136,12 +142,12 @@ Note that these contexts are creating Location objects not just simple points. T
|
|||
isn't obvious until the ``PolarLocations`` context is used which can also rotate objects within
|
||||
its scope - much as the hour and minute indicator on an analogue clock.
|
||||
|
||||
Also note that the locations are local to the current workplane(s). However, it's easy for a user
|
||||
to retrieve the global locations relative to the current workplane(s) as follows:
|
||||
Also note that the locations are local to the current location(s) - i.e. ``Locations`` can be
|
||||
nested. It's easy for a user to retrieve the global locations:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
with Workplanes(Plane.XY, Plane.XZ):
|
||||
with Locations(Plane.XY, Plane.XZ):
|
||||
locs = GridLocations(1, 1, 2, 2)
|
||||
for l in locs:
|
||||
print(l)
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ class Hinge(Compound):
|
|||
add(hinge_profile.part, rotation=(90, 0, 0), mode=Mode.INTERSECT)
|
||||
|
||||
# Create holes for fasteners
|
||||
with Workplanes(leaf_builder.part.faces().filter_by(Axis.Y)[-1]):
|
||||
with Locations(leaf_builder.part.faces().filter_by(Axis.Y)[-1]):
|
||||
with GridLocations(0, length / 3, 1, 3):
|
||||
holes = CounterSinkHole(3 * MM, 5 * MM)
|
||||
# Add the hinge pin to the external leaf
|
||||
|
|
@ -194,7 +194,7 @@ with BuildPart() as box_builder:
|
|||
with Locations((-15 * CM, 0, 5 * CM)):
|
||||
Box(2 * CM, 12 * CM, 4 * MM, mode=Mode.SUBTRACT)
|
||||
bbox = box.bounding_box()
|
||||
with Workplanes(
|
||||
with Locations(
|
||||
Plane(origin=(bbox.min.X, 0, bbox.max.Z - 30 * MM), z_dir=(-1, 0, 0))
|
||||
):
|
||||
with GridLocations(0, 40 * MM, 1, 3):
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ __all__ = [
|
|||
"HexLocations",
|
||||
"PolarLocations",
|
||||
"Locations",
|
||||
"Workplanes",
|
||||
"GridLocations",
|
||||
"BuildLine",
|
||||
"BuildPart",
|
||||
|
|
|
|||
|
|
@ -186,9 +186,9 @@ class Builder(ABC):
|
|||
|
||||
# If there are no workplanes, create a default XY plane
|
||||
if not self.workplanes and not WorkplaneList._get_context():
|
||||
self.workplanes_context = Workplanes(Plane.XY).__enter__()
|
||||
self.workplanes_context = WorkplaneList(Plane.XY).__enter__()
|
||||
elif self.workplanes:
|
||||
self.workplanes_context = Workplanes(*self.workplanes).__enter__()
|
||||
self.workplanes_context = WorkplaneList(*self.workplanes).__enter__()
|
||||
|
||||
return self
|
||||
|
||||
|
|
@ -541,27 +541,25 @@ class Builder(ABC):
|
|||
and self.__class__.__name__ not in operations_apply_to[validating_class]
|
||||
):
|
||||
raise RuntimeError(
|
||||
f"{self.__class__.__name__} doesn't have a "
|
||||
f"{validating_class} object or operation "
|
||||
f"({validating_class} applies to {operations_apply_to[validating_class]})"
|
||||
f"({validating_class} doesn't apply to {operations_apply_to[validating_class]})"
|
||||
)
|
||||
# Check for valid object inputs
|
||||
for obj in objects:
|
||||
operation = (
|
||||
validating_class
|
||||
if isinstance(validating_class, str)
|
||||
else validating_class.__class__.__name__
|
||||
)
|
||||
if obj is None:
|
||||
pass
|
||||
elif isinstance(obj, Builder):
|
||||
raise RuntimeError(
|
||||
f"{validating_class.__class__.__name__} doesn't accept Builders as input,"
|
||||
f"{operation} doesn't accept Builders as input,"
|
||||
f" did you intend <{obj.__class__.__name__}>.{obj._obj_name}?"
|
||||
)
|
||||
elif isinstance(obj, list):
|
||||
raise RuntimeError(
|
||||
f"{validating_class.__class__.__name__} doesn't accept {type(obj).__name__},"
|
||||
f" did you intend *{obj}?"
|
||||
)
|
||||
elif not isinstance(obj, Shape):
|
||||
raise RuntimeError(
|
||||
f"{validating_class.__class__.__name__} doesn't accept {type(obj).__name__},"
|
||||
f"{operation} doesn't accept {type(obj).__name__},"
|
||||
f" did you intend <keyword>={obj}?"
|
||||
)
|
||||
|
||||
|
|
@ -915,7 +913,7 @@ class WorkplaneList:
|
|||
at all time.
|
||||
|
||||
Args:
|
||||
planes (list[Plane]): list of planes
|
||||
workplanes (sequence of Union[Face, Plane, Location]): objects to become planes
|
||||
|
||||
"""
|
||||
|
||||
|
|
@ -924,9 +922,16 @@ class WorkplaneList:
|
|||
"WorkplaneList._current"
|
||||
)
|
||||
|
||||
def __init__(self, planes: list[Plane]):
|
||||
def __init__(self, *workplanes: Union[Face, Plane, Location]):
|
||||
self._reset_tok = None
|
||||
self.workplanes = planes
|
||||
self.workplanes = []
|
||||
for plane in workplanes:
|
||||
if isinstance(plane, Plane):
|
||||
self.workplanes.append(plane)
|
||||
elif isinstance(plane, (Location, Face)):
|
||||
self.workplanes.append(Plane(plane))
|
||||
else:
|
||||
raise ValueError(f"WorkplaneList does not accept {type(plane)}")
|
||||
self.locations_context = None
|
||||
self.plane_index = 0
|
||||
|
||||
|
|
@ -998,35 +1003,6 @@ class WorkplaneList:
|
|||
return result
|
||||
|
||||
|
||||
class Workplanes(WorkplaneList):
|
||||
"""Workplane Context: Workplanes
|
||||
|
||||
Create workplanes from the given sequence of planes.
|
||||
|
||||
Args:
|
||||
objs (Union[Face, Plane, Location]): sequence of faces, planes, or
|
||||
locations to use to define workplanes.
|
||||
Raises:
|
||||
ValueError: invalid input
|
||||
"""
|
||||
|
||||
def __init__(self, *objs: Union[Face, Plane, Location]):
|
||||
# warnings.warn(
|
||||
# "Workplanes may be deprecated - Post on Discord to save it",
|
||||
# DeprecationWarning,
|
||||
# stacklevel=2,
|
||||
# )
|
||||
self.workplanes = []
|
||||
for obj in objs:
|
||||
if isinstance(obj, Plane):
|
||||
self.workplanes.append(obj)
|
||||
elif isinstance(obj, (Location, Face)):
|
||||
self.workplanes.append(Plane(obj))
|
||||
else:
|
||||
raise ValueError(f"Workplanes does not accept {type(obj)}")
|
||||
super().__init__(self.workplanes)
|
||||
|
||||
|
||||
#
|
||||
# To avoid import loops, Vector add & sub are monkey-patched
|
||||
def _vector_add(self: Vector, vec: VectorLike) -> Vector:
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ def add(
|
|||
|
||||
object_iter = objects if isinstance(objects, Iterable) else [objects]
|
||||
|
||||
validate_inputs(context, None, object_iter)
|
||||
validate_inputs(context, "add", object_iter)
|
||||
|
||||
if isinstance(context, BuildPart):
|
||||
if rotation is None:
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ license:
|
|||
|
||||
"""
|
||||
import unittest
|
||||
from math import pi
|
||||
from build123d import *
|
||||
from build123d import Builder, WorkplaneList, LocationList
|
||||
|
||||
|
|
@ -233,18 +234,30 @@ class TestShapeList(unittest.TestCase):
|
|||
Box(1, 1, 1)
|
||||
self.assertEqual(len(test.part.vertices()), 8)
|
||||
self.assertTrue(isinstance(test.part.vertices(), ShapeList))
|
||||
with self.assertRaises(ValueError):
|
||||
with BuildPart() as test:
|
||||
Box(1, 1, 1)
|
||||
v = test.vertices("ALL")
|
||||
|
||||
def test_edges(self):
|
||||
with BuildPart() as test:
|
||||
Box(1, 1, 1)
|
||||
self.assertEqual(len(test.part.edges()), 12)
|
||||
self.assertTrue(isinstance(test.part.edges(), ShapeList))
|
||||
with self.assertRaises(ValueError):
|
||||
with BuildPart() as test:
|
||||
Box(1, 1, 1)
|
||||
v = test.edges("ALL")
|
||||
|
||||
def test_wires(self):
|
||||
with BuildPart() as test:
|
||||
Box(1, 1, 1)
|
||||
self.assertEqual(len(test.wires()), 6)
|
||||
self.assertTrue(isinstance(test.wires(), ShapeList))
|
||||
with self.assertRaises(ValueError):
|
||||
with BuildPart() as test:
|
||||
Box(1, 1, 1)
|
||||
v = test.wires("ALL")
|
||||
|
||||
def test_wires_last(self):
|
||||
with BuildPart() as test:
|
||||
|
|
@ -258,12 +271,20 @@ class TestShapeList(unittest.TestCase):
|
|||
Box(1, 1, 1)
|
||||
self.assertEqual(len(test.part.faces()), 6)
|
||||
self.assertTrue(isinstance(test.part.faces(), ShapeList))
|
||||
with self.assertRaises(ValueError):
|
||||
with BuildPart() as test:
|
||||
Box(1, 1, 1)
|
||||
v = test.faces("ALL")
|
||||
|
||||
def test_solids(self):
|
||||
with BuildPart() as test:
|
||||
Box(1, 1, 1)
|
||||
self.assertEqual(len(test.part.solids()), 1)
|
||||
self.assertTrue(isinstance(test.part.solids(), ShapeList))
|
||||
with self.assertRaises(ValueError):
|
||||
with BuildPart() as test:
|
||||
Box(1, 1, 1)
|
||||
v = test.solids("ALL")
|
||||
|
||||
def test_compounds(self):
|
||||
with BuildPart() as test:
|
||||
|
|
@ -271,6 +292,11 @@ class TestShapeList(unittest.TestCase):
|
|||
self.assertEqual(len(test.part.compounds()), 1)
|
||||
self.assertTrue(isinstance(test.part.compounds(), ShapeList))
|
||||
|
||||
def test_shapes(self):
|
||||
with BuildPart() as test:
|
||||
Box(1, 1, 1)
|
||||
self.assertIsNone(test._shapes(Compound))
|
||||
|
||||
|
||||
class TestBuilder(unittest.TestCase):
|
||||
"""Test the Builder base class"""
|
||||
|
|
@ -287,73 +313,23 @@ class TestBuilder(unittest.TestCase):
|
|||
make_face()
|
||||
self.assertEqual(len(outer.pending_faces), 2)
|
||||
|
||||
|
||||
class TestWorkplanes(unittest.TestCase):
|
||||
def test_named(self):
|
||||
with Workplanes(Plane.XY) as test:
|
||||
self.assertTupleAlmostEquals(
|
||||
test.workplanes[0].origin.to_tuple(), (0, 0, 0), 5
|
||||
)
|
||||
self.assertTupleAlmostEquals(
|
||||
test.workplanes[0].z_dir.to_tuple(), (0, 0, 1), 5
|
||||
)
|
||||
|
||||
def test_locations(self):
|
||||
with Workplanes(Plane.XY):
|
||||
with Locations((0, 0, 1), (0, 0, 2)) as l:
|
||||
with Workplanes(*l.locations) as w:
|
||||
origins = [p.origin.to_tuple() for p in w.workplanes]
|
||||
self.assertTupleAlmostEquals(origins[0], (0, 0, 1), 5)
|
||||
self.assertTupleAlmostEquals(origins[1], (0, 0, 2), 5)
|
||||
self.assertEqual(len(origins), 2)
|
||||
|
||||
def test_grid_locations(self):
|
||||
with Workplanes(Plane(origin=(1, 2, 3))):
|
||||
locs = GridLocations(4, 6, 2, 2).locations
|
||||
self.assertTupleAlmostEquals(locs[0].position.to_tuple(), (-1, -1, 3), 5)
|
||||
self.assertTupleAlmostEquals(locs[1].position.to_tuple(), (-1, 5, 3), 5)
|
||||
self.assertTupleAlmostEquals(locs[2].position.to_tuple(), (3, -1, 3), 5)
|
||||
self.assertTupleAlmostEquals(locs[3].position.to_tuple(), (3, 5, 3), 5)
|
||||
|
||||
def test_conversions(self):
|
||||
loc = Location((1, 2, 3), (23, 45, 67))
|
||||
loc2 = Workplanes(loc).workplanes[0].to_location()
|
||||
self.assertTupleAlmostEquals(loc.to_tuple()[0], loc2.to_tuple()[0], 6)
|
||||
self.assertTupleAlmostEquals(loc.to_tuple()[1], loc2.to_tuple()[1], 6)
|
||||
|
||||
loc = Location((-10, -2, 30), (-123, 145, 267))
|
||||
face = Face.make_rect(1, 1).move(loc)
|
||||
loc2 = Workplanes(face).workplanes[0].to_location()
|
||||
face2 = Face.make_rect(1, 1).move(loc2)
|
||||
self.assertTupleAlmostEquals(
|
||||
face.center().to_tuple(), face2.center().to_tuple(), 6
|
||||
)
|
||||
self.assertTupleAlmostEquals(
|
||||
face.normal_at(face.center()).to_tuple(),
|
||||
face2.normal_at(face2.center()).to_tuple(),
|
||||
6,
|
||||
)
|
||||
|
||||
def test_bad_plane(self):
|
||||
with self.assertRaises(ValueError):
|
||||
with BuildPart(4):
|
||||
pass
|
||||
|
||||
def test_locations_after_new_workplane(self):
|
||||
with BuildPart(Plane.XY):
|
||||
with Locations((0, 1, 2), (3, 4, 5)):
|
||||
with BuildPart(Plane.XY.offset(2)):
|
||||
self.assertTupleAlmostEquals(
|
||||
LocationList._get_context().locations[0].position.to_tuple(),
|
||||
(0, 0, 2),
|
||||
5,
|
||||
)
|
||||
def test_plane_with_no_x(self):
|
||||
with BuildPart() as p:
|
||||
Box(1, 1, 1)
|
||||
front = p.faces().sort_by(Axis.X)[-1]
|
||||
with BuildSketch(front):
|
||||
offset(front, amount=-0.1)
|
||||
extrude(amount=0.1)
|
||||
self.assertAlmostEqual(p.part.volume, 1**3 + 0.1 * (1 - 2 * 0.1) ** 2, 4)
|
||||
|
||||
def test_no_workplane(self):
|
||||
with BuildSketch() as s:
|
||||
Circle(1)
|
||||
|
||||
|
||||
class TestWorkplaneList(unittest.TestCase):
|
||||
def test_iter(self):
|
||||
for i, plane in enumerate(WorkplaneList([Plane.XY, Plane.YZ])):
|
||||
for i, plane in enumerate(WorkplaneList(Plane.XY, Plane.YZ)):
|
||||
if i == 0:
|
||||
self.assertTrue(plane == Plane.XY)
|
||||
elif i == 1:
|
||||
|
|
@ -365,35 +341,47 @@ class TestWorkplaneList(unittest.TestCase):
|
|||
self.assertTupleAlmostEquals(pnts[0].to_tuple(), (0, 1, 2), 5)
|
||||
self.assertTupleAlmostEquals(pnts[1].to_tuple(), (0, 2, 3), 5)
|
||||
|
||||
def test_invalid_workplane(self):
|
||||
with self.assertRaises(ValueError):
|
||||
WorkplaneList(Vector(1, 1, 1))
|
||||
|
||||
|
||||
class TestValidateInputs(unittest.TestCase):
|
||||
# def test_no_builder(self):
|
||||
# with self.assertRaises(RuntimeError):
|
||||
# Circle(1)
|
||||
|
||||
def test_wrong_builder(self):
|
||||
with self.assertRaises(RuntimeError):
|
||||
with self.assertRaises(RuntimeError) as rte:
|
||||
with BuildPart():
|
||||
Circle(1)
|
||||
self.assertEqual(
|
||||
"BuildPart doesn't have a Circle object or operation (Circle applies to ['BuildSketch'])",
|
||||
str(rte.exception),
|
||||
)
|
||||
|
||||
def test_bad_builder_input(self):
|
||||
with self.assertRaises(RuntimeError):
|
||||
with self.assertRaises(RuntimeError) as rte:
|
||||
with BuildPart() as p:
|
||||
Box(1, 1, 1)
|
||||
with BuildSketch():
|
||||
add(p)
|
||||
self.assertEqual(
|
||||
"add doesn't accept Builders as input, did you intend <BuildPart>.part?",
|
||||
str(rte.exception),
|
||||
)
|
||||
|
||||
def test_no_sequence(self):
|
||||
with self.assertRaises(ValueError):
|
||||
with self.assertRaises(ValueError) as rte:
|
||||
with BuildPart() as p:
|
||||
Box(1, 1, 1)
|
||||
fillet([None, None], radius=1)
|
||||
self.assertEqual("3D fillet operation takes only Edges", str(rte.exception))
|
||||
|
||||
def test_wrong_type(self):
|
||||
with self.assertRaises(RuntimeError):
|
||||
with self.assertRaises(RuntimeError) as rte:
|
||||
with BuildPart() as p:
|
||||
Box(1, 1, 1)
|
||||
fillet(4, radius=1)
|
||||
self.assertEqual(
|
||||
"fillet doesn't accept int, did you intend <keyword>=4?", str(rte.exception)
|
||||
)
|
||||
|
||||
|
||||
class TestBuilderExit(unittest.TestCase):
|
||||
|
|
@ -522,6 +510,23 @@ class TestLocations(unittest.TestCase):
|
|||
loc.orientation.to_tuple(), Location(Plane.XZ).orientation.to_tuple(), 5
|
||||
)
|
||||
|
||||
def test_from_plane(self):
|
||||
with BuildPart():
|
||||
loc = Locations(Plane.XY.offset(1)).locations[0]
|
||||
self.assertTupleAlmostEquals(loc.position.to_tuple(), (0, 0, 1), 5)
|
||||
|
||||
def test_from_axis(self):
|
||||
with BuildPart():
|
||||
loc = Locations(Axis((1, 1, 1), (0, 0, 1))).locations[0]
|
||||
self.assertTupleAlmostEquals(loc.position.to_tuple(), (1, 1, 1), 5)
|
||||
|
||||
def test_multiplication(self):
|
||||
circles = GridLocations(2, 2, 2, 2) * Circle(1)
|
||||
self.assertEqual(len(circles), 4)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
GridLocations(2, 2, 2, 2) * "error"
|
||||
|
||||
|
||||
class TestVectorExtensions(unittest.TestCase):
|
||||
def test_vector_localization(self):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue