Skip to content

Commit 93623fa

Browse files
authored
#788 Add __repr__ and __str__ methods to Matrix (#791)
* #788 Add __repr__ and __str__ methods to Matrix * Shortened code with join & improved test * Renamed variable and fixed use of join * Replaced __getitem__ with transposed_list * Optimized repr and removed str methods * Cleanup comment * Increased geom.py code coverage to near 100% * Reformatted with black 19.10b0 defaults * Remove the typing import
1 parent 4c45eb2 commit 93623fa

File tree

2 files changed

+201
-1
lines changed

2 files changed

+201
-1
lines changed

cadquery/occ_impl/geom.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,14 @@ def __getitem__(self, rc: Tuple[int, int]) -> float:
362362
else:
363363
raise IndexError("Out of bounds access into 4x4 matrix: {!r}".format(rc))
364364

365+
def __repr__(self) -> str:
366+
"""
367+
Generate a valid python expression representing this Matrix
368+
"""
369+
matrix_transposed = self.transposed_list()
370+
matrix_str = ",\n ".join(str(matrix_transposed[i::4]) for i in range(4))
371+
return f"Matrix([{matrix_str}])"
372+
365373

366374
class Plane(object):
367375
"""A 2D coordinate system in space

tests/test_cad_objects.py

Lines changed: 193 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ def testVectorConstructors(self):
4747
v9.z = 3.0
4848
self.assertTupleAlmostEquals((1, 2, 3), (v9.x, v9.y, v9.z), 4)
4949

50+
with self.assertRaises(TypeError):
51+
Vector("vector")
52+
with self.assertRaises(TypeError):
53+
Vector(1, 2, 3, 4)
54+
5055
def testVertex(self):
5156
"""
5257
Tests basic vertex functions
@@ -66,6 +71,46 @@ def testBasicBoundingBox(self):
6671
# OCC uses some approximations
6772
self.assertAlmostEqual(bb1.xlen, 1.0, 1)
6873

74+
# Test adding to an existing bounding box
75+
v0 = Vertex.makeVertex(0, 0, 0)
76+
bb2 = v0.BoundingBox().add(v.BoundingBox())
77+
78+
bb3 = bb1.add(bb2)
79+
self.assertTupleAlmostEquals((2, 2, 2), (bb3.xlen, bb3.ylen, bb3.zlen), 7)
80+
81+
bb3 = bb2.add((3, 3, 3))
82+
self.assertTupleAlmostEquals((3, 3, 3), (bb3.xlen, bb3.ylen, bb3.zlen), 7)
83+
84+
bb3 = bb2.add(Vector(3, 3, 3))
85+
self.assertTupleAlmostEquals((3, 3, 3), (bb3.xlen, bb3.ylen, bb3.zlen), 7)
86+
87+
# Test 2D bounding boxes
88+
bb1 = (
89+
Vertex.makeVertex(1, 1, 0)
90+
.BoundingBox()
91+
.add(Vertex.makeVertex(2, 2, 0).BoundingBox())
92+
)
93+
bb2 = (
94+
Vertex.makeVertex(0, 0, 0)
95+
.BoundingBox()
96+
.add(Vertex.makeVertex(3, 3, 0).BoundingBox())
97+
)
98+
bb3 = (
99+
Vertex.makeVertex(0, 0, 0)
100+
.BoundingBox()
101+
.add(Vertex.makeVertex(1.5, 1.5, 0).BoundingBox())
102+
)
103+
# Test that bb2 contains bb1
104+
self.assertEqual(bb2, BoundBox.findOutsideBox2D(bb1, bb2))
105+
self.assertEqual(bb2, BoundBox.findOutsideBox2D(bb2, bb1))
106+
# Test that neither bounding box contains the other
107+
self.assertIsNone(BoundBox.findOutsideBox2D(bb1, bb3))
108+
109+
# Test creation of a bounding box from a shape - note the low accuracy comparison
110+
# as the box is a little larger than the shape
111+
bb1 = BoundBox._fromTopoDS(Solid.makeCylinder(1, 1).wrapped, optimal=False)
112+
self.assertTupleAlmostEquals((2, 2, 1), (bb1.xlen, bb1.ylen, bb1.zlen), 1)
113+
69114
def testEdgeWrapperCenter(self):
70115
e = self._make_circle()
71116

@@ -276,6 +321,20 @@ def testVectorProject(self):
276321
point.toTuple(), (59 / 7, 55 / 7, 51 / 7), decimal_places
277322
)
278323

324+
def testVectorNotImplemented(self):
325+
v = Vector(1, 2, 3)
326+
with self.assertRaises(NotImplementedError):
327+
v.distanceToLine()
328+
with self.assertRaises(NotImplementedError):
329+
v.projectToLine()
330+
with self.assertRaises(NotImplementedError):
331+
v.distanceToPlane()
332+
333+
def testVectorSpecialMethods(self):
334+
v = Vector(1, 2, 3)
335+
self.assertEqual(repr(v), "Vector: (1.0, 2.0, 3.0)")
336+
self.assertEqual(str(v), "Vector: (1.0, 2.0, 3.0)")
337+
279338
def testMatrixCreationAndAccess(self):
280339
def matrix_vals(m):
281340
return [[m[r, c] for c in range(4)] for r in range(4)]
@@ -320,7 +379,9 @@ def matrix_vals(m):
320379
]
321380
with self.assertRaises(ValueError):
322381
Matrix(invalid)
323-
382+
# Test input with invalid type
383+
with self.assertRaises(TypeError):
384+
Matrix("invalid")
324385
# Test input with invalid size / nested types
325386
with self.assertRaises(TypeError):
326387
Matrix([[1, 2, 3, 4], [1, 2, 3], [1, 2, 3, 4]])
@@ -340,6 +401,77 @@ def matrix_vals(m):
340401
with self.assertRaises(IndexError):
341402
m["ab"]
342403

404+
# test __repr__ methods
405+
m = Matrix(vals4x4)
406+
mRepr = "Matrix([[1.0, 0.0, 0.0, 1.0],\n [0.0, 1.0, 0.0, 2.0],\n [0.0, 0.0, 1.0, 3.0],\n [0.0, 0.0, 0.0, 1.0]])"
407+
self.assertEqual(repr(m), mRepr)
408+
self.assertEqual(str(eval(repr(m))), mRepr)
409+
410+
def testMatrixFunctionality(self):
411+
# Test rotate methods
412+
def matrix_almost_equal(m, target_matrix):
413+
for r, row in enumerate(target_matrix):
414+
for c, target_value in enumerate(row):
415+
self.assertAlmostEqual(m[r, c], target_value)
416+
417+
root_3_over_2 = math.sqrt(3) / 2
418+
m_rotate_x_30 = [
419+
[1, 0, 0, 0],
420+
[0, root_3_over_2, -1 / 2, 0],
421+
[0, 1 / 2, root_3_over_2, 0],
422+
[0, 0, 0, 1],
423+
]
424+
mx = Matrix()
425+
mx.rotateX(30 * DEG2RAD)
426+
matrix_almost_equal(mx, m_rotate_x_30)
427+
428+
m_rotate_y_30 = [
429+
[root_3_over_2, 0, 1 / 2, 0],
430+
[0, 1, 0, 0],
431+
[-1 / 2, 0, root_3_over_2, 0],
432+
[0, 0, 0, 1],
433+
]
434+
my = Matrix()
435+
my.rotateY(30 * DEG2RAD)
436+
matrix_almost_equal(my, m_rotate_y_30)
437+
438+
m_rotate_z_30 = [
439+
[root_3_over_2, -1 / 2, 0, 0],
440+
[1 / 2, root_3_over_2, 0, 0],
441+
[0, 0, 1, 0],
442+
[0, 0, 0, 1],
443+
]
444+
mz = Matrix()
445+
mz.rotateZ(30 * DEG2RAD)
446+
matrix_almost_equal(mz, m_rotate_z_30)
447+
448+
# Test matrix multipy vector
449+
v = Vector(1, 0, 0)
450+
self.assertTupleAlmostEquals(
451+
mz.multiply(v).toTuple(), (root_3_over_2, 1 / 2, 0), 7
452+
)
453+
454+
# Test matrix multipy matrix
455+
m_rotate_xy_30 = [
456+
[root_3_over_2, 0, 1 / 2, 0],
457+
[1 / 4, root_3_over_2, -root_3_over_2 / 2, 0],
458+
[-root_3_over_2 / 2, 1 / 2, 3 / 4, 0],
459+
[0, 0, 0, 1],
460+
]
461+
mxy = mx.multiply(my)
462+
matrix_almost_equal(mxy, m_rotate_xy_30)
463+
464+
# Test matrix inverse
465+
vals4x4 = [[1, 2, 3, 4], [5, 1, 6, 7], [8, 9, 1, 10], [0, 0, 0, 1]]
466+
vals4x4_invert = [
467+
[-53 / 144, 25 / 144, 1 / 16, -53 / 144],
468+
[43 / 144, -23 / 144, 1 / 16, -101 / 144],
469+
[37 / 144, 7 / 144, -1 / 16, -107 / 144],
470+
[0, 0, 0, 1],
471+
]
472+
m = Matrix(vals4x4).inverse()
473+
matrix_almost_equal(m, vals4x4_invert)
474+
343475
def testTranslate(self):
344476
e = Edge.makeCircle(2, (1, 2, 3))
345477
e2 = e.translate(Vector(0, 0, 1))
@@ -394,6 +526,55 @@ def testPlaneNotEqual(self):
394526
Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 1, 1)),
395527
)
396528

529+
def testInvalidPlane(self):
530+
# Test plane creation error handling
531+
with self.assertRaises(ValueError):
532+
Plane.named("XX", (0, 0, 0))
533+
with self.assertRaises(ValueError):
534+
Plane(origin=(0, 0, 0), xDir=(0, 0, 0), normal=(0, 1, 1))
535+
with self.assertRaises(ValueError):
536+
Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 0, 0))
537+
538+
def testPlaneMethods(self):
539+
# Test error checking
540+
p = Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 1, 0))
541+
with self.assertRaises(ValueError):
542+
p.toLocalCoords("box")
543+
with self.assertRaises(NotImplementedError):
544+
p.mirrorInPlane([Solid.makeBox(1, 1, 1)], "Z")
545+
546+
# Test translation to local coordinates
547+
local_box = Workplane(p.toLocalCoords(Solid.makeBox(1, 1, 1)))
548+
local_box_vertices = [(v.X, v.Y, v.Z) for v in local_box.vertices().vals()]
549+
target_vertices = [
550+
(0, -1, 0),
551+
(0, 0, 0),
552+
(0, -1, 1),
553+
(0, 0, 1),
554+
(1, -1, 0),
555+
(1, 0, 0),
556+
(1, -1, 1),
557+
(1, 0, 1),
558+
]
559+
for i, target_point in enumerate(target_vertices):
560+
self.assertTupleAlmostEquals(target_point, local_box_vertices[i], 7)
561+
562+
# Test mirrorInPlane
563+
mirror_box = Workplane(p.mirrorInPlane([Solid.makeBox(1, 1, 1)], "Y")[0])
564+
mirror_box_vertices = [(v.X, v.Y, v.Z) for v in mirror_box.vertices().vals()]
565+
target_vertices = [
566+
(0, 0, 1),
567+
(0, 0, 0),
568+
(0, -1, 1),
569+
(0, -1, 0),
570+
(-1, 0, 1),
571+
(-1, 0, 0),
572+
(-1, -1, 1),
573+
(-1, -1, 0),
574+
]
575+
for i, target_point in enumerate(target_vertices):
576+
self.assertTupleAlmostEquals(target_point, mirror_box_vertices[i], 7)
577+
397578
def testLocation(self):
398579

399580
# Vector
@@ -418,6 +599,17 @@ def testLocation(self):
418599
== loc3.wrapped.Transformation().TranslationPart().Z()
419600
)
420601

602+
# Test creation from the OCP.gp.gp_Trsf object
603+
loc4 = Location(gp_Trsf())
604+
self.assertTupleAlmostEquals(loc4.toTuple()[0], (0, 0, 0), 7)
605+
self.assertTupleAlmostEquals(loc4.toTuple()[1], (0, 0, 0), 7)
606+
607+
# Test error handling on creation
608+
with self.assertRaises(TypeError):
609+
Location((0, 0, 1))
610+
with self.assertRaises(TypeError):
611+
Location("xy_plane")
612+
421613
def testEdgeWrapperRadius(self):
422614

423615
# get a radius from a simple circle

0 commit comments

Comments
 (0)