def cylinder(direction_or_base, length_or_cap, r=None, d=None): """Generate a cylinder. Signatures (convenience forms only): * cylinder(direction, length, radius) * cylinder(direction, length, d = diameter) * cylinder(base, cap, radius) * cylinder(base, cap, d = diameter) """ # Radius or diameter radius = _get_radius(r, d) # length_or_base must be a vector or a number if number.valid(length_or_cap): # Number - direction/length return Frustum.direction_length(direction_or_base, length_or_cap, radius, radius) elif Vector.valid_type(length_or_cap): # Vector type - base/cap return Frustum(direction_or_base, length_or_cap, radius, radius) else: raise TypeError( "Invalid call signature: length_or_cap must be a vector type or a number" )
def cone(direction_or_base, length_or_tip, r=None, d=None): """Generate a cone. For the forms with a direction, the base will be at the origin. Signatures (convenience forms only): * cone(direction, length, radius) * cone(direction, length, d = diameter) * cone(base, cap, radius) * cone(base, cap, d = diameter) """ # TODO should use r = None, *, d = None? (not just here) # Radius or diameter radius = _get_radius(r, d) # length_or_base must be a vector or a number if number.valid(length_or_tip): # Number - direction/length return Frustum.direction_length(direction_or_base, length_or_tip, radius, 0) elif Vector.valid_type(length_or_tip): # Vector type - base/cap return Frustum(direction_or_base, length_or_tip, radius, 0) else: raise TypeError( "Invalid call signature: length_or_cap must be a vector type or a number" )
def frustum(direction_or_base, length_or_cap, r=None, d=None): """Generate a frustum. For the forms with a direction, the base will be at the origin. Signatures (convenience forms only): * frustum (direction, length, radii) * frustum (direction, length, d = diameters) * frustum (base, cap, radii) * frustum (base, cap, d = diameters) """ # Radius or diameter radii = _get_radii(r, d) # length_or_base must be a vector or a number if number.valid(length_or_cap): # Number - direction/length return Frustum.direction_length(direction_or_base, length_or_cap, *radii) elif Vector.valid_type(length_or_cap): # Vector type - base/cap return Frustum(direction_or_base, length_or_cap, *radii) else: raise TypeError( "Invalid call signature: length_or_cap must be a vector type or a number" )
def test_construction(self): with self.assertNothingRaised(): Frustum(X, Y, 1, 0) # Zero size with self.assertWarnsRegex(UserWarning, r'length is 0'): Frustum(X, X, 1, 2) with self.assertWarnsRegex(UserWarning, r'length is 0'): Frustum(X, (1, 0, 0), 1, 2) # Zero is not allowed as a shortcut with self.assertRaises(TypeError): Frustum(0, Y, 1, 0) with self.assertRaises(TypeError): Frustum(X, 0, 1, 0) # Invalid Frustum(X, Y, 1, 2) # Reference with self.assertRaises(TypeError): Frustum(1, Y, 1, 2) # Base is not a vector with self.assertRaises(TypeError): Frustum(X, 1, 1, 2) # Cap is not a vector with self.assertRaises(TypeError): Frustum(X, Y, X, 2) # Base radius is not a number with self.assertRaises(TypeError): Frustum(X, Y, 1, X) # Base radius is not a number
def test_to_scad(self): sphere = Sphere(2) cube = Cuboid(10, 10, 10) cylinder = Frustum(origin, Z, 11, 11) self.assertEqual( Difference([sphere, cube, cylinder]).to_scad(), ScadObject("difference", None, None, [ sphere.to_scad(), cube.to_scad(), cylinder.to_scad(), ]))
def test_sum(self): sphere = Sphere(2) cube = Cuboid(10, 10, 10) cylinder = Frustum(origin, Z, 11, 11) objects = [sphere, cube, cylinder] self.assertEqual(sum(objects, Union.empty()), Union(objects))
def test_to_scad(self): sphere = Sphere(2) cube = Cuboid(10, 10, 10) cylinder = Frustum(origin, Z, 11, 11) self.assertEqual( Union([sphere, cube, cylinder]).to_scad(), ScadObject("union", None, None, [ sphere.to_scad(), cube.to_scad(), cylinder.to_scad(), ])) # Empty self.assertEqual( Union([]).to_scad(), ScadObject("union", None, None, None))
def test_construction(self): sphere = Sphere(11) cube = Cuboid(22, 22, 22) cylinder = Frustum(origin, Z, 11, 11) object_list = [sphere, cube, cylinder] # Empty Csg([]) # With objects self.assertEqual(Csg(object_list).children, object_list) # With generator self.assertEqual(Csg(o for o in object_list).children, object_list) # With invalid objects with self.assertRaises(TypeError): Csg(None) # None instead of empty list with self.assertRaises(TypeError): Csg([Sphere]) # Class instead of object with self.assertRaises(TypeError): Csg(sphere) # Object instead of list with self.assertRaises(TypeError): Csg([None]) # List with invalid value with self.assertRaises(TypeError): Csg([sphere, None]) # List with object and invalid value
def test_to_scad(self): sphere = Sphere(2) cube = Cuboid(10, 10, 10) cylinder = Frustum(origin, Z, 5, 5) mixed = Union( [Intersection([sphere, cylinder]), Difference([cube, sphere])]) self.assertEqual( mixed.to_scad(), ScadObject("union", None, None, [ ScadObject( "intersection", None, None, [sphere.to_scad(), cylinder.to_scad()]), ScadObject("difference", None, None, [cube.to_scad(), sphere.to_scad()]), ]))
def test_csg_generators(self): sphere = Sphere(11) cuboid = Cuboid(11, 22, 33) cylinder = Frustum(origin, Z, 11, 11) object_list = [sphere, cuboid, cylinder] self.assertEqual(union(object_list), Union(object_list)) self.assertEqual(difference(object_list), Difference(object_list)) self.assertEqual(intersection(object_list), Intersection(object_list))
def test_equality(self): sphere = Sphere(11) cube = Cuboid(22, 22, 22) cylinder = Frustum(origin, Z, 11, 11) objects = [sphere, cube, cylinder] # Different types of CSG are not equal, even if their children are identical self.assertNotEqual(Union(objects), Intersection(objects)) self.assertNotEqual(Intersection(objects), Difference(objects)) self.assertNotEqual(Difference(objects), Union(objects))
def test_direction_length(self): self.assertEqual(Frustum.direction_length(X, 5, 1, 2), Frustum(origin, X * 5, 1, 2)) self.assertEqual(Frustum.direction_length((1, 0, 0), 5, 1, 2), Frustum(origin, X * 5, 1, 2)) # Zero direction with self.assertRaises(ValueError): Frustum.direction_length(origin, 5, 1, 2) # Zero size with self.assertWarnsRegex(UserWarning, r'length is 0'): Frustum.direction_length(X, 0, 1, 2)
def test_multiplication(self): a = Sphere(2) b = Cuboid(10, 10, 10) c = Frustum(origin, Z, 11, 11) d = Cuboid(20, 20, 20) self.assertEqual( a * b , Intersection([a, b ])) self.assertEqual( c * c , Intersection([c, c ])) self.assertEqual( (a * b) * c , Intersection([a, b, c ])) self.assertEqual( a * (b * c) , Intersection([a, b, c ])) self.assertEqual( a * b * c * d , Intersection([a, b, c, d])) self.assertEqual( ((a * b) * c) * d , Intersection([a, b, c, d])) self.assertEqual( a * (b * (c * d)), Intersection([a, b, c, d])) self.assertEqual( (a * b) * (c * d) , Intersection([a, b, c, d]))
def test_subtraction(self): a = Sphere(2) b = Cuboid(10, 10, 10) c = Frustum(origin, Z, 11, 11) d = Cuboid(20, 20, 20) # Difference is non-associative, so we get nested differences self.assertEqual( a - b , Difference([a, b ])) self.assertEqual( c - c , Difference([c, c ])) self.assertEqual( (a - b) - c , Difference([a, b, c ])) self.assertEqual( a - (b - c) , Difference([a, Difference([b, c])])) self.assertEqual( a - b - c - d , Difference([a, b, c, d])) self.assertEqual( ((a - b) - c) - d , Difference([a, b, c, d])) self.assertEqual( a - (b - (c - d)), Difference([a, Difference([b, Difference([c, d])])])) self.assertEqual( (a - b) - (c - d) , Difference([Difference([a, b]), Difference([c, d])]))
def test_addition(self): a = Sphere(2) b = Cuboid(10, 10, 10) c = Frustum(origin, Z, 11, 11) d = Cuboid(20, 20, 20) self.assertEqual( a + b , Union([a, b ])) self.assertEqual( c + c , Union([c, c ])) self.assertEqual( (a + b) + c , Union([a, b, c ])) self.assertEqual( a + (b + c) , Union([a, b, c ])) self.assertEqual( a + b + c + d , Union([a, b, c, d])) self.assertEqual( ((a + b) + c) + d , Union([a, b, c, d])) self.assertEqual( a + (b + (c + d)), Union([a, b, c, d])) self.assertEqual( (a + b) + (c + d) , Union([a, b, c, d])) # Empty union self.assertEqual(Union([]) + Union([]), Union([])) self.assertEqual(Union([]) + a, Union([a])) self.assertEqual(a + Union([]), Union([a]))
def test_equality(self): sphere = Sphere(11) cuboid = Cuboid(11, 22, 33) cylinder = Frustum(origin, Z, 11, 11) objects = [sphere, cuboid, cylinder] # Same object self.assertEqualToItself(Union([])) self.assertEqualToItself(Union(objects)) # Equal objects self.assertEqual(Union([]), Union([])) self.assertEqual(Union(objects), Union(objects)) # Different objects self.assertNotEqual(Union(objects), Union([sphere, cuboid])) self.assertNotEqual(Union([cuboid, sphere]), Union([sphere, cuboid])) # Equal objects from different specifications self.assertEqual(Union.empty(), Union([]))
def test_render_to_file(self): part = Sphere(0.6) + Cuboid(1, 2, 3) - Frustum([0, 0, 0], [0, 0, 4], 0.5, 0.5) expected_file_name = os.path.join(os.path.dirname(__file__), "test_scad_object_render_to_file_expected.scad") actual_file_name = os.path.join(os.path.dirname(__file__), "test_scad_object_render_to_file_actual.scad") self.assertTrue(os.path.isfile(expected_file_name)) self.assertFalse(os.path.exists(actual_file_name)) try: render_to_file(part, actual_file_name, fn=60) with open(actual_file_name) as actual_file: actual = actual_file.read() with open(expected_file_name) as expected_file: expected = expected_file.read() self.assertNotEqual(expected, "") self.assertEqual(actual, expected) finally: if os.path.exists(actual_file_name): os.unlink(actual_file_name)
def test_frustum_generators(self): v0 = Vector(0, 0, 0) # Cylinder - base/cap self.assertEqual(cylinder(X, Y, 2), Frustum(X, Y, 2, 2)) self.assertEqual(cylinder(X, Y, r=2), Frustum(X, Y, 2, 2)) self.assertEqual(cylinder(X, Y, d=4), Frustum(X, Y, 2, 2)) self.assertEqual(cylinder(origin, Y, 2), Frustum(origin, Y, 2, 2)) # 0 is allowed as base # 0 is not allowed as cap because it would be interpreted as # direction/length # Cylinder - direction/length self.assertEqual(cylinder(X, 1, 2), Frustum(origin, X, 2, 2)) self.assertEqual(cylinder(X, 1, r=2), Frustum(origin, X, 2, 2)) self.assertEqual(cylinder(X, 1, d=4), Frustum(origin, X, 2, 2)) # Cylinder - invalid with self.assertNothingRaised(): cylinder(X, 5, 1) # Reference with self.assertRaises(TypeError): cylinder(5, 5, 1) # First is not a vector with self.assertRaises(TypeError): cylinder(X, "", 1) # Second is not a vector or number with self.assertRaises(TypeError): cylinder(X, 5) # Neither radius nor diameter with self.assertRaises(TypeError): cylinder(X, 5, 1, 1) # Both radius and diameter with self.assertRaises(ValueError): cylinder(v0, 5, 1) # Zero vector is not allowed as direction with self.assertRaises(TypeError): cylinder(X, 5, r=(1, 2)) # Radii with self.assertRaises(TypeError): cylinder(X, 5, d=(1, 2)) # Diameters # Cone - base/tip self.assertEqual(cone(X, Y, 2), Frustum(X, Y, 2, 0)) self.assertEqual(cone(X, Y, r=2), Frustum(X, Y, 2, 0)) self.assertEqual(cone(X, Y, d=4), Frustum(X, Y, 2, 0)) # Cone - direction/length self.assertEqual(cone(X, 1, 2), Frustum(origin, X, 2, 0)) self.assertEqual(cone(X, 1, r=2), Frustum(origin, X, 2, 0)) self.assertEqual(cone(X, 1, d=4), Frustum(origin, X, 2, 0)) # Cone - invalid with self.assertNothingRaised(): cone(X, 5, 1) # Reference with self.assertRaises(TypeError): cone(5, 5, 1) # First is not a vector with self.assertRaises(TypeError): cone(X, "", 1) # Second is not a vector or number with self.assertRaises(TypeError): cone(X, 5) # Neither radius nor diameter with self.assertRaises(TypeError): cone(X, 5, 1, 1) # Both radius and diameter with self.assertRaises(ValueError): cone(v0, 5, 1) # Zero vector is not allowed as direction with self.assertRaises(TypeError): cone(v0, 5, r=(1, 2)) # Radii with self.assertRaises(TypeError): cone(v0, 5, d=(1, 2)) # Diameters # Frustum - base/cap self.assertEqual(frustum(X, Y, (2, 3)), Frustum(X, Y, 2, 3)) self.assertEqual(frustum(X, Y, r=(2, 3)), Frustum(X, Y, 2, 3)) self.assertEqual(frustum(X, Y, d=(4, 6)), Frustum(X, Y, 2, 3)) # Frustum - direction/length self.assertEqual(frustum(X, 1, (2, 3)), Frustum(origin, X, 2, 3)) self.assertEqual(frustum(X, 1, r=(2, 3)), Frustum(origin, X, 2, 3)) self.assertEqual(frustum(X, 1, d=(4, 6)), Frustum(origin, X, 2, 3)) # Frustum - invalid with self.assertNothingRaised(): frustum(X, 5, (1, 2)) # Reference with self.assertRaises(TypeError): frustum(5, 5, (1, 2)) # First is not a vector with self.assertRaises(TypeError): frustum(X, "", (1, 2)) # Second is not a vector or number with self.assertRaises(TypeError): frustum(X, 5) # Neither radii nor diameters with self.assertRaises(TypeError): frustum(X, 5, (1, 2), (1, 2)) # Both radii and diameters with self.assertRaises(ValueError): frustum(v0, 5, (2, 3)) # Zero vector is not allowed as direction with self.assertRaises(TypeError): frustum(X, 1, r=2) # Single radius with self.assertRaises(TypeError): frustum(X, 1, d=4) # Single diameter # Errors caught by _get_radii with self.assertRaises(TypeError): frustum(X, 1, r=(2, 3), d=(4, 6)) # Both radius and diameter with self.assertRaises(TypeError): frustum(X, 1) # Neither radius nor diameter with self.assertRaises(ValueError): frustum(X, 1, r=(2, 3, 4)) # Too many radii with self.assertRaises(ValueError): frustum(X, 1, d=(4, 6, 8)) # Too many diameters with self.assertRaises(ValueError): frustum(X, 1, r=(2, )) # Too few radii with self.assertRaises(ValueError): frustum(X, 1, d=(4, )) # Too few diameters
def test_repr(self): self.assertRepr(Frustum(origin, 5 * X, 1, 2), "Frustum(Vector(0, 0, 0), Vector(5, 0, 0), 1, 2)")
def test_equality(self): # Same object self.assertEqualToItself(Frustum(origin, X, 2, 1)) # Equal objects self.assertEqual(Frustum(X, Y, 1, 2), Frustum(X, Y, 1, 2)) self.assertEqual(Frustum(X, origin, 1, 2), Frustum(X, origin, 1, 2)) # Different objects self.assertNotEqual(Frustum(X, Y, 1, 2), Frustum(Y, X, 1, 2)) self.assertNotEqual(Frustum(X, Y, 1, 2), Frustum(X, Y, 2, 1)) self.assertNotEqual(Frustum(X, Y, 1, 2), Frustum(Y, X, 2, 1)) # Equal objects from different specifications self.assertEqual(Frustum(X, Y, 1, 2), Frustum((1, 0, 0), Y, 1, 2))
def test_str(self): self.assertStr( Frustum(Y, 5 * X, 1, 2), "Frustum with base <0, 1, 0> (base radius 1) and cap <5, 0, 0> (cap radius 2)" )
def test_to_scad(self): self.ignore_scad_comments = True # Cylinder along Z axis self.assertScadObject(Frustum(origin, 5 * Z, 1, 1), "cylinder", [5.0], [('r', 1)]) # Cone along Z axis self.assertScadObject(Frustum(origin, 5 * Z, 1, 0), "cylinder", [5.0], [('r1', 1), ('r2', 0)]) # Frustum along Z axis self.assertScadObject(Frustum(origin, 5 * Z, 2, 1), "cylinder", [5.0], [('r1', 2), ('r2', 1)]) # Cylinder along other axes (X, Y, -X, -Y, -Z) cylinder_scad = ScadObject("cylinder", [5], [('r', 1)], None) self.assertEqual( Frustum(origin, 5 * X, 1, 1).to_scad(), ScadObject("rotate", [], [('a', 90.0), ('v', [0.0, 1.0, 0.0])], [cylinder_scad])) self.assertEqual( Frustum(origin, 5 * Y, 1, 1).to_scad(), ScadObject("rotate", [], [('a', 90.0), ('v', [-1.0, 0.0, 0.0])], [cylinder_scad])) self.assertEqual( Frustum(origin, -5 * X, 1, 1).to_scad(), ScadObject("rotate", [], [('a', 90.0), ('v', [0.0, -1.0, 0.0])], [cylinder_scad])) self.assertEqual( Frustum(origin, -5 * Y, 1, 1).to_scad(), ScadObject("rotate", [], [('a', 90.0), ('v', [1.0, 0.0, 0.0])], [cylinder_scad])) self.assertEqual( Frustum(origin, -5 * Z, 1, 1).to_scad(), ScadObject("rotate", [], [('a', 180.0), ('v', [1.0, 0.0, 0.0])], [cylinder_scad])) # Parallel to Z axis (shifted along X, X/Y, X/Y/Z) cylinder_scad = ScadObject("cylinder", [5], [('r', 1)], None) self.assertEqual( Frustum([1, 0, 0], [1, 0, 5], 1, 1).to_scad(), ScadObject("translate", [[1, 0, 0]], [], [cylinder_scad])) self.assertEqual( Frustum([1, 2, 0], [1, 2, 5], 1, 1).to_scad(), ScadObject("translate", [[1, 2, 0]], [], [cylinder_scad])) self.assertEqual( Frustum([1, 2, 3], [1, 2, 8], 1, 1).to_scad(), ScadObject("translate", [[1, 2, 3]], [], [cylinder_scad])) # Parallel to other axis (X) cylinder_scad = ScadObject("cylinder", [5], [('r', 1)], None) self.assertEqual( Frustum([0, 1, 2], [5, 1, 2], 1, 1).to_scad(), ScadObject("translate", [[0, 1, 2]], [], [ ScadObject("rotate", [], [('a', 90.0), ('v', [0.0, 1.0, 0.0])], [cylinder_scad]) ])) # Not parallel to axis (in the XZ plane without and with translation) cylinder_scad = ScadObject("cylinder", [5 * math.sqrt(2)], [('r', 1)], None) self.assertEqual( Frustum([0, 0, 0], [5, 0, 5], 1, 1).to_scad(), ScadObject("rotate", [], [('a', 45.0), ('v', [0.0, 1.0, 0.0])], [cylinder_scad])) self.assertEqual( Frustum([5, 0, 0], [0, 0, 5], 1, 1).to_scad(), ScadObject("translate", [[5, 0, 0]], [], [ ScadObject("rotate", [], [('a', 45.0), ('v', [0.0, -1.0, 0.0])], [cylinder_scad]) ]))