def _equivalent(self): yaw, pitch, roll = self._ypr # yaw-pitch-roll in local coordinates corresponds to roll-pitch-yaw in # global coordinates. transforms = [] if yaw != 0 or pitch != 0: transforms.append(RotateXyz(pitch, 0 , yaw)) if roll != 0 : transforms.append(RotateXyz(0 , roll, 0 )) return Chained(transforms)
def to_scad(self, target): equivalent = self._equivalent() if equivalent == Chained([]): # The equivalent transform is empty (this can happen if yaw, pitch, # and roll are all 0). This would result in an empty ScadObject # (with children if target is not None). Instead, generate an empty # rotation for improved clarity of the generated code. equivalent = RotateXyz(0, 0, 0) return equivalent.to_scad(target).comment(str(self))
def test_equality(self): # Same object self.assertEqualToItself(RotateXyz( 60, 30, 15 )) # Equal objects self.assertEqual(RotateXyz( 60, 30, 15 ), RotateXyz( 60, 30, 15 )) # Different objects self.assertNotEqual(RotateXyz( 60, 30, 15 ), RotateXyz( 60, 30, 16 )) # Equal objects from different specifications pass
def test_rotate(self): # Canonical axis/angle self.assertEqual(rotate(axis = Vector(1, 2, 3), angle = 45), RotateAxisAngle(Vector(1, 2, 3), 45)) # Vector self.assertEqual(rotate(axis = [1, 2, 3], angle = 45), RotateAxisAngle(Vector(1, 2, 3), 45)) # List # Canonical from/to self.assertEqual(rotate(frm = Vector(1, 2, 3), to = Vector(4, 5, 6)), RotateFromTo(Vector(1, 2, 3), Vector(4, 5, 6))) # Vectors self.assertEqual(rotate(frm = [1, 2, 3], to = [4, 5, 6]), RotateFromTo(Vector(1, 2, 3), Vector(4, 5, 6))) # Lists # Canonical XYZ self.assertEqual(rotate(xyz = [45, 0, 30]), RotateXyz(45, 0, 30)) # Canonical yaw/pitch/roll self.assertEqual(rotate(ypr = [90, -20, 5]), RotateYpr(90, -20, 5)) # Convenience axis/angle (implicit) self.assertEqual(rotate(Vector(1, 2, 3), 45), RotateAxisAngle(Vector(1, 2, 3), 45)) # Vector self.assertEqual(rotate( [1, 2, 3], 45), RotateAxisAngle(Vector(1, 2, 3), 45)) # List # Convenience axis/angle (explicit) self.assertEqual(rotate(Vector(1, 2, 3), angle = 45), RotateAxisAngle(Vector(1, 2, 3), 45)) # Vector self.assertEqual(rotate( [1, 2, 3], angle = 45), RotateAxisAngle(Vector(1, 2, 3), 45)) # List # Convenience from/to (implicit) self.assertEqual(rotate(X , Y ), RotateFromTo(X, Y)) # Vectors self.assertEqual(rotate([1, 0, 0], [0, 1, 0]), RotateFromTo(X, Y)) # Lists # Convenience from/to (explicit) self.assertEqual(rotate(X , to = Y ), RotateFromTo(X, Y)) # Vectors self.assertEqual(rotate([1, 0, 0], to = [0, 1, 0]), RotateFromTo(X, Y)) # Lists
def test_multiplication(self): # Create some transform r = RotateXyz(60, 30, 15) s = ScaleAxes (1, 2, -1) t = Translate([30, 20, 10]) # 2-chained self.assertEqual(r * s, Chained([r, s])) # 3-chained self.assertEqual( r * s * t , Chained([r, s, t])) self.assertEqual( (r * s) * t , Chained([r, s, t])) self.assertEqual( r * (s * t) , Chained([r, s, t])) # Multiplication is associative, but not commutative (kind-of already follows from the other tests) self.assertEqual ( (r * s) * t, r * (s * t) ) self.assertNotEqual( r * s , s * r ) # 4-chained self.assertEqual( r * s * r * s , Chained([r, s, r, s])) self.assertEqual( (r * s) * (r * s) , Chained([r, s, r, s])) self.assertEqual( ((r * s) * r) * s , Chained([r, s, r, s])) self.assertEqual( r * (s * (r * s)) , Chained([r, s, r, s])) rs = r * s self.assertEqual( rs * rs , Chained([r, s, r, s])) # Empty chained self.assertEqual(Chained([]) * Chained([]), Chained([])) self.assertEqual(Chained([]) * r, Chained([r])) self.assertEqual(r * Chained([]), Chained([r]))
def test_str(self): self.assertStr(RotateXyz(0, 0, 0), "Rotate by 0° around X, Y, and Z") self.assertStr(RotateXyz(1, 0, 0), "Rotate by 1° around X") self.assertStr(RotateXyz(0, 2, 0), "Rotate by 2° around Y") self.assertStr(RotateXyz(0, 0, 3), "Rotate by 3° around Z") self.assertStr(RotateXyz(1, 2, 0), "Rotate by 1° around X and 2° around Y") self.assertStr(RotateXyz(1, 0, 3), "Rotate by 1° around X and 3° around Z") self.assertStr(RotateXyz(0, 2, 3), "Rotate by 2° around Y and 3° around Z") self.assertStr(RotateXyz(1, 2, 3), "Rotate by 1° around X, 2° around Y, and 3° around Z")
def to_scad(self, target): axis, angle = self._to_axis_angle() if axis is None: # No rotation. Generate a zero XYZ transform instead of simply # returning the target. This improves code clarity and also ensures # that a valid ScadObject is returned even if target is None. return RotateXyz(0, 0, 0).to_scad(target).comment(str(self)) else: # Yes rotation return RotateAxisAngle(axis.normalized(), angle).to_scad(target).comment(str(self))
def test_inequality(self): # Different-type transformations are not equal (even if the values are identical) transforms = [ RotateAxisAngle(X, 0), RotateFromTo(X, X), RotateXyz(0, 0, 0), RotateYpr(0, 0, 0), ScaleAxisFactor(X, 1), ScaleUniform(1), ScaleAxes (1, 1, 1), Translate([0, 0, 0]), ] for t1 in transforms: for t2 in transforms: if t1 is not t2: self.assertNotEqual(t1, t2)
def test_to_scad(self): r = RotateXyz(60, 30, 15) s = ScaleAxes(1, 2, -1) cube = Cuboid(11, 11, 11) # Simple transform self.assertEqual( Transformed(r, cube).to_scad(), ScadObject("rotate", [[60, 30, 15]], None, [ ScadObject("cube", [[11, 11, 11]], None, None), ])) # Chained transform self.assertEqual( Transformed(Chained([r, s]), cube).to_scad(), ScadObject("rotate", [[60, 30, 15]], None, [ ScadObject("scale", [[1, 2, -1]], None, [ ScadObject("cube", [[11, 11, 11]], None, None), ]) ]))
def test_equality(self): r = RotateXyz(60, 30, 15) s = ScaleAxes(1, 2, -1) t = Translate([60, 30, 15]) c = Chained([r, s, t]) # Same object self.assertEqualToItself(c) self.assertEqualToItself(Chained([])) # Equal objects self.assertEqual(Chained([]), Chained([])) self.assertEqual(Chained([r, s, t]), Chained([r, s, t])) # Identical children self.assertEqual( Chained([ RotateXyz(60, 30, 15), ScaleAxes(60, 30, 15), Translate([60, 30, 15]) ]), Chained([ RotateXyz(60, 30, 15), ScaleAxes(60, 30, 15), Translate([60, 30, 15]) ])) # Equal children # Different objects self.assertNotEqual(Chained([r, s, t]), Chained([r, s])) # Different number of children self.assertNotEqual(Chained([r, s, t]), Chained([r, t, s])) # Different order of children self.assertNotEqual( Chained([ RotateXyz(60, 30, 15), RotateXyz(60, 30, 15), Translate([60, 30, 15]) ]), Chained([ RotateXyz(60, 30, 15), RotateXyz(60, 30, 15), Translate([60, 30, 16]) ])) # Unequal children
def test_to_scad(self): # Create some transforms r = RotateXyz(60, 30, 15) s = ScaleAxes(1, 2, -1) t = Translate([30, 20, 10]) # Non-empty chained with None target self.assertEqual( Chained([r, s, t]).to_scad(None), ScadObject("rotate", [[60, 30, 15]], None, [ ScadObject( "scale", [[1, 2, -1]], None, [ScadObject("translate", [[30, 20, 10]], None, None)]) ])) # Empty chained with valid target dummy = ScadObject("dummy", None, None, None) self.assertEqual(Chained([]).to_scad(dummy), dummy) # Empty chained with None target self.assertEqual( Chained([]).to_scad(None), ScadObject(None, None, None, None))
def test_postfix_transform(self): cube = Cuboid(11, 11, 11) # Vectors rv = [60, 34, 30] sv = [ 2, 1, 1] tv = [10, 20, 30] # Transforms r = RotateXyz(*rv) s = ScaleAxes (*sv) t = Translate(tv) # Long shortcuts self.assertEqual(cube.rotate (xyz = rv).scale(sv) .translate(tv), t * s * r * cube) self.assertEqual(cube.transform(r) .scale(sv) .transform(t) , t * s * r * cube) self.assertEqual(cube.rotate (xyz = rv).transform(s).translate(tv), t * s * r * cube) self.assertEqual(cube.transform(s * r) .transform(t) , t * s * r * cube) self.assertEqual(cube.scale([1, 2, 3]), ScaleAxes(1, 2, 3) * cube) self.assertEqual(cube.scale(2), ScaleUniform(2) * cube) self.assertEqual(cube.scale([1, 2, 3], 4), ScaleAxisFactor([1, 2, 3], 4) * cube) # Error with self.assertRaises(TypeError): cube.transform(cube)
def test_to_matrix(self): t = Translate([1, 2, 3]) r = RotateXyz(0, 0, 90) # First rotate, then translate self.assertAlmostEqual( Chained([t, r]).to_matrix().row_values, [ [0, -1, 0, 1], [1, 0, 0, 2], [0, 0, 1, 3], [0, 0, 0, 1], ]) # First translate, then rotate self.assertAlmostEqual( Chained([r, t]).to_matrix().row_values, [ [0, -1, 0, -2], [1, 0, 0, 1], [0, 0, 1, 3], [0, 0, 0, 1], ]) # Empty self.assertIdentity(Chained([]).to_matrix())
def rotate(axis_or_frm = None, angle_or_to = None, axis = None, angle = None, frm = None, to = None, xyz = None, ypr = None, ignore_ambiguity = False): """Generate a rotation around an axis through the origin. Signatures (canonical forms): * rotate(axis = x, angle = 45) * rotate(frm = x, to = y) * rotate(xyz = [45, 0, 30]) * rotate(ypr = [45, -30, 10]) Signatures (convenience forms): * rotate(x, 45) * rotate(x, angle = 45) * rotate(x, y) * rotate(x, to = y) """ # Canonical forms: # axis_or_axmag_or_frm angle_or_to axis angle frm to xyz ypr # - - vec num - - - - # Axis/angle # - - - - vec vec - - # From/to # - - - - - - list - # XYZ # - - - - - - - list # Yaw/pitch/roll # Convenience forms (-: must be None, *: overwritten) # vec num * * - - - - # Axis/angle (implicit) # vec - * num - - - - # Axis/angle (explicit) # vec vec - - * * - - # From/to (implicit) # vec - - - * vec - - # From/to (explicit) # # "Vector type" is Vector, list, or tuple # Make sure that there are no conflicts between convenience parameters and canonical parameters if both(axis_or_frm, axis ): raise TypeError("axis" " cannot be specified together with axis_or_frm") if both(axis_or_frm, frm ): raise TypeError("frm" " cannot be specified together with axis_or_frm") if both(angle_or_to, angle): raise TypeError("angle" " cannot be specified together with angle_or_to") if both(angle_or_to, to ): raise TypeError("to" " cannot be specified together with angle_or_to") # Transform the convenience forms to canonical form if axis_or_frm is not None: if not Vector.valid_type(axis_or_frm): raise TypeError("axis must be a vector type") if angle_or_to is not None: if number.valid(angle_or_to): # Axis/angle (implicit) axis = axis_or_frm angle = angle_or_to elif Vector.valid_type(angle_or_to): # From/to (implicit) frm = axis_or_frm to = angle_or_to else: raise TypeError("angle_or_to must be a number or a vector type") elif angle is not None: # Axis/angle (explicit) axis = axis_or_frm elif to is not None: # From/to (explicit) frm = axis_or_frm # Check the parameters that must appear in pairs if axis is not None and angle is None: raise TypeError("angle" " is required when " "axis" " is given") if angle is not None and axis is None: raise TypeError("axis" " is required when " "angle" " is given") if frm is not None and to is None: raise TypeError("to" " is required when " "frm" " is given") if to is not None and frm is None: raise TypeError("frm" " is required when " "to" " is given") # Handle the different cases if axis is not None: # Check that no other specification is given if frm is not None: raise TypeError("frm" " cannot be specified together with axis") if xyz is not None: raise TypeError("xyz" " cannot be specified together with axis") if ypr is not None: raise TypeError("ypr" " cannot be specified together with axis") return RotateAxisAngle(axis, angle) elif frm is not None: # Check that no other specification is given if axis is not None: raise TypeError("axis" " cannot be specified together with frm") if xyz is not None: raise TypeError("xyz" " cannot be specified together with frm") if ypr is not None: raise TypeError("ypr" " cannot be specified together with frm") return RotateFromTo(frm, to, ignore_ambiguity) elif xyz is not None: # Check that no other specification is given if axis is not None: raise TypeError("axis" " cannot be specified together with frm") if frm is not None: raise TypeError("frm" " cannot be specified together with axis") if ypr is not None: raise TypeError("ypr" " cannot be specified together with axis") return RotateXyz(*xyz) elif ypr is not None: # Check that no other specification is given if axis is not None: raise TypeError("axis" " cannot be specified together with frm") if frm is not None: raise TypeError("frm" " cannot be specified together with axis") if xyz is not None: raise TypeError("xyz" " cannot be specified together with axis") return RotateYpr(*ypr) else: raise TypeError("Invalid call signature")
def test_construction(self): # Valid r = RotateXyz( 60, 30, 15 ) # Invalid with self.assertRaises(TypeError): RotateXyz(1, 2, "3")
def test_to_matrix(self): # No rotation self.assertAlmostEqual(RotateXyz( 0 , 0, 0).to_matrix(), affine_matrix(X, Y, Z)) # 90 degrees around a single axis self.assertAlmostEqual(RotateXyz(90, 0, 0).to_matrix(), affine_matrix( X, Z, -Y)) self.assertAlmostEqual(RotateXyz( 0, 90, 0).to_matrix(), affine_matrix(-Z, Y, X)) self.assertAlmostEqual(RotateXyz( 0, 0, 90).to_matrix(), affine_matrix( Y, -X, Z)) # 180 degrees around a single axis self.assertAlmostEqual(RotateXyz(180, 0, 0).to_matrix(), affine_matrix( X, -Y, -Z)) self.assertAlmostEqual(RotateXyz( 0, 180, 0).to_matrix(), affine_matrix(-X, Y, -Z)) self.assertAlmostEqual(RotateXyz( 0, 0, 180).to_matrix(), affine_matrix(-X, -Y, Z)) # 90 degrees each around two axes self.assertAlmostEqual(RotateXyz(90, 90, 0).to_matrix(), affine_matrix(-Z, X, -Y)) self.assertAlmostEqual(RotateXyz(90, 0, 90).to_matrix(), affine_matrix( Y, Z, X)) self.assertAlmostEqual(RotateXyz( 0, 90, 90).to_matrix(), affine_matrix(-Z, -X, Y)) # 90 degrees each around all three axes self.assertAlmostEqual(RotateXyz(90, 90, 90).to_matrix(), affine_matrix(-Z, Y, X))
def test_repr(self): self.assertRepr(RotateXyz(1, 2, 3), "RotateXyz(1, 2, 3)")
def test_inverse(self): self.assertInverse(RotateXyz(30, 60, 90), RotateXyz(-30, 0, 0) * RotateXyz(0, -60, 0) * RotateXyz(0, 0, -90), symmetric=False)
from cadlib.object.primitives import Cuboid, Cylinder, Sphere from cadlib.transform.primitives import RotateXyz, ScaleAxes, Translate from cadlib.util.vector import Z def test_case(name, object): print(name) print(" Cadlib tree:") print(object.to_tree().format(top_indent=" ")) print(" OpenSCAD tree:") print(object.to_scad().to_tree().format(top_indent=" ")) print(" OpenSCAD source:") print(object.to_scad().to_code(top_indent=" ")) o1 = Cuboid(10, 10, 10) o2 = Cylinder(Z, 5, 5) o3 = Sphere(2) rotate = RotateXyz(90, 0, 45) scale = ScaleAxes(1, 1, 2) translate = Translate([10, 10, 2]) test_case("Translated object", translate * o1) test_case("Intersection", o1 * o2 * o3) test_case("Complex", rotate * (translate * scale * (o2 + o3) + o1))
def test_to_scad(self): r = RotateXyz(0, 0, 0) self.assertScadObjectTarget(r, None, "rotate", [[0, 0, 0]], None, None) r = RotateXyz(60, 30, 15) self.assertScadObjectTarget(r, None, "rotate", [[60, 30, 15]], None, None)
from cadlib.object.primitives import Cuboid from cadlib.scad import render_to_file from cadlib.transform.primitives import RotateXyz object = Cuboid(2, 3, 4) rotate = RotateXyz([20, 0, 45]) assembly = rotate * object render_to_file(assembly, "adhoc_test.scad")