From a4807fd3abe437529d797cabaef75798a8283f2f Mon Sep 17 00:00:00 2001 From: Arthur Moore Date: Tue, 30 Apr 2024 07:43:09 -0400 Subject: [PATCH] OpenScad Python Runner - Make camera_args a variable As opposed to being a parameter of `create_image`. Also made CameraArguments support fluent. --- tests/openscad_runner.py | 31 ++++++++++++++++++++++++----- tests/test_bins.py | 26 ++++++++++++------------- tests/test_holes.py | 41 +++++++++++++++------------------------ tests/test_spiral_vase.py | 14 ++++++------- 4 files changed, 62 insertions(+), 50 deletions(-) diff --git a/tests/openscad_runner.py b/tests/openscad_runner.py index d6c5418..c73987d 100644 --- a/tests/openscad_runner.py +++ b/tests/openscad_runner.py @@ -2,6 +2,7 @@ Helpful classes for running OpenScad from Python. @Copyright Arthur Moore 2024 MIT License """ +from __future__ import annotations import json import subprocess @@ -24,17 +25,27 @@ class Vec3(NamedTuple): y: float z: float -@dataclass +@dataclass(frozen=True) class CameraArguments: """ Controls the camera position when outputting to png format. @see `openscad -h`. + Supports fluid interface. """ translate: Vec3 rotate: Vec3 distance: float - def as_argument(self): + def with_translation(self, new_translate: Vec3) -> CameraArguments: + return CameraArguments(translate=new_translate, rotate=self.rotate, distance=self.distance) + + def with_rotation(self, new_rotate: Vec3) -> CameraArguments: + return CameraArguments(translate=self.translate, rotate=new_rotate, distance=self.distance) + + def with_distance(self, new_distance: float) -> CameraArguments: + return CameraArguments(translate=self.translate, rotate=rotate, distance=new_distance) + + def as_argument(self) -> str: return '--camera=' \ f'{",".join(map(str,self.translate))},{",".join(map(str,self.rotate))},{self.distance}' @@ -79,6 +90,13 @@ def set_variable_argument(var: str, val: str) -> [str, str]: """ return ['-D', f'{var}={str(val)}'] +class CameraRotations: + '''Pre-defined useful camera rotations''' + Default = Vec3(0,0,0), + AngledTop = Vec3(45,0,45) + AngledBottom = Vec3(225,0,225) + Top = Vec3(45,0,0) + class OpenScadRunner: '''Helper to run the openscad binary''' scad_file_path: Path @@ -88,7 +106,7 @@ class OpenScadRunner: '''If set, a temporary parameter file is created, and used with these variables''' WINDOWS_DEFAULT_PATH = 'C:\\Program Files\\OpenSCAD\\openscad.exe' - TOP_ANGLE_CAMERA = CameraArguments(Vec3(0,0,0),Vec3(45,0,45),50) + TOP_ANGLE_CAMERA = CameraArguments(Vec3(0,0,0),Vec3(45,0,45),150) common_arguments = [ #'--hardwarnings', // Does not work when setting variables by using functions @@ -106,9 +124,10 @@ class OpenScadRunner: self.openscad_binary_path = self.WINDOWS_DEFAULT_PATH self.scad_file_path = file_path self.image_folder_base = Path('.') + self.camera_arguments = None self.parameters = None - def create_image(self, camera_args: CameraArguments, args: [str], image_file_name: str): + def create_image(self, args: [str], image_file_name: str): """ Run the code, to create an image. @Important The only verification is that no errors occured. @@ -119,11 +138,13 @@ class OpenScadRunner: image_path = self.image_folder_base.joinpath(image_file_name) command_arguments = self.common_arguments + \ - [camera_args.as_argument()] + args + \ + ([self.camera_arguments.as_argument()] if self.camera_arguments != None else []) + \ + args + \ ["-o", str(image_path), str(self.scad_file_path)] #print(command_arguments) if self.parameters != None: + #print(self.parameters) params = ParameterFile(parameterSets={"python_generated": self.parameters}) with NamedTemporaryFile(prefix="gridfinity-rebuilt-", suffix=".json", mode='wt',delete_on_close=False) as file: json.dump(params, file, sort_keys=True, indent=2, cls=DataClassJSONEncoder) diff --git a/tests/test_bins.py b/tests/test_bins.py index be364a2..1c922b0 100644 --- a/tests/test_bins.py +++ b/tests/test_bins.py @@ -23,26 +23,26 @@ class TestBinHoles(unittest.TestCase): parameter_file_path = Path("gridfinity-rebuilt-bins.json") parameter_file_data = ParameterFile.from_json(parameter_file_path.read_text()) cls.default_parameters = parameter_file_data.parameterSets["Default"] - cls.camera_args = CameraArguments(Vec3(0,0,0),Vec3(225,0,225),150) def setUp(self): self.scad_runner = OpenScadRunner(Path('../gridfinity-rebuilt-bins.scad')) self.scad_runner.image_folder_base = Path('../images/base_hole_options/') self.scad_runner.parameters = self.default_parameters.copy() + self.scad_runner.camera_arguments = CameraArguments(Vec3(0,0,0), CameraRotations.AngledBottom, 150) def test_no_holes(self): vars = self.scad_runner.parameters vars["refined_holes"] = False vars["magnet_holes"] = False vars["screw_holes"] = False - self.scad_runner.create_image(self.camera_args, [], Path('no_holes.png')) + self.scad_runner.create_image([], Path('no_holes.png')) def test_refined_holes(self): vars = self.scad_runner.parameters vars["refined_holes"] = True vars["magnet_holes"] = False vars["screw_holes"] = False - self.scad_runner.create_image(self.camera_args, [], Path('refined_holes.png')) + self.scad_runner.create_image([], Path('refined_holes.png')) def test_refined_and_screw_holes(self): vars = self.scad_runner.parameters @@ -50,7 +50,7 @@ class TestBinHoles(unittest.TestCase): vars["magnet_holes"] = False vars["screw_holes"] = True vars["printable_hole_top"] = False - self.scad_runner.create_image(self.camera_args, [], Path('refined_and_screw_holes.png')) + self.scad_runner.create_image([], Path('refined_and_screw_holes.png')) def test_screw_holes_plain(self): vars = self.scad_runner.parameters @@ -58,7 +58,7 @@ class TestBinHoles(unittest.TestCase): vars["magnet_holes"] = False vars["screw_holes"] = True vars["printable_hole_top"] = False - self.scad_runner.create_image(self.camera_args, [], Path('screw_holes_plain.png')) + self.scad_runner.create_image([], Path('screw_holes_plain.png')) def test_screw_holes_printable(self): vars = self.scad_runner.parameters @@ -66,7 +66,7 @@ class TestBinHoles(unittest.TestCase): vars["magnet_holes"] = False vars["screw_holes"] = True vars["printable_hole_top"] = True - self.scad_runner.create_image(self.camera_args, [], Path('screw_holes_printable.png')) + self.scad_runner.create_image([], Path('screw_holes_printable.png')) def test_magnet_holes_plain(self): vars = self.scad_runner.parameters @@ -76,7 +76,7 @@ class TestBinHoles(unittest.TestCase): vars["crush_ribs"] = False vars["chamfer_holes"] = False vars["printable_hole_top"] = False - self.scad_runner.create_image(self.camera_args, [], Path('magnet_holes_plain.png')) + self.scad_runner.create_image([], Path('magnet_holes_plain.png')) def test_magnet_holes_chamfered(self): vars = self.scad_runner.parameters @@ -86,7 +86,7 @@ class TestBinHoles(unittest.TestCase): vars["crush_ribs"] = False vars["chamfer_holes"] = True vars["printable_hole_top"] = False - self.scad_runner.create_image(self.camera_args, [], Path('magnet_holes_chamfered.png')) + self.scad_runner.create_image([], Path('magnet_holes_chamfered.png')) def test_magnet_holes_printable(self): vars = self.scad_runner.parameters @@ -96,7 +96,7 @@ class TestBinHoles(unittest.TestCase): vars["crush_ribs"] = False vars["chamfer_holes"] = False vars["printable_hole_top"] = True - self.scad_runner.create_image(self.camera_args, [], Path('magnet_holes_printable.png')) + self.scad_runner.create_image([], Path('magnet_holes_printable.png')) def test_magnet_holes_with_crush_ribs(self): vars = self.scad_runner.parameters @@ -106,7 +106,7 @@ class TestBinHoles(unittest.TestCase): vars["crush_ribs"] = True vars["chamfer_holes"] = False vars["printable_hole_top"] = False - self.scad_runner.create_image(self.camera_args, [], Path('magnet_holes_with_crush_ribs.png')) + self.scad_runner.create_image([], Path('magnet_holes_with_crush_ribs.png')) def test_magnet_and_screw_holes_plain(self): vars = self.scad_runner.parameters @@ -116,7 +116,7 @@ class TestBinHoles(unittest.TestCase): vars["crush_ribs"] = False vars["chamfer_holes"] = False vars["printable_hole_top"] = False - self.scad_runner.create_image(self.camera_args, [], Path('magnet_and_screw_holes_plain.png')) + self.scad_runner.create_image([], Path('magnet_and_screw_holes_plain.png')) def test_magnet_and_screw_holes_printable(self): vars = self.scad_runner.parameters @@ -126,7 +126,7 @@ class TestBinHoles(unittest.TestCase): vars["crush_ribs"] = False vars["chamfer_holes"] = False vars["printable_hole_top"] = True - self.scad_runner.create_image(self.camera_args, [], Path('magnet_and_screw_holes_printable.png')) + self.scad_runner.create_image([], Path('magnet_and_screw_holes_printable.png')) def test_magnet_and_screw_holes_all(self): vars = self.scad_runner.parameters @@ -136,7 +136,7 @@ class TestBinHoles(unittest.TestCase): vars["crush_ribs"] = True vars["chamfer_holes"] = True vars["printable_hole_top"] = True - self.scad_runner.create_image(self.camera_args, [], Path('magnet_and_screw_holes_all.png')) + self.scad_runner.create_image([], Path('magnet_and_screw_holes_all.png')) if __name__ == '__main__': unittest.main() diff --git a/tests/test_holes.py b/tests/test_holes.py index 9308b58..70e9bbf 100644 --- a/tests/test_holes.py +++ b/tests/test_holes.py @@ -15,73 +15,64 @@ class TestHoleCutouts(unittest.TestCase): Currently only makes sure code runs, and outputs pictures for manual verification. """ - @classmethod - def setUpClass(cls): - cls.scad_runner = OpenScadRunner(Path('../gridfinity-rebuilt-holes.scad')) - cls.scad_runner.image_folder_base = Path('../images/hole_cutouts/') + def setUp(self): + self.scad_runner = OpenScadRunner(Path('../gridfinity-rebuilt-holes.scad')) + self.scad_runner.image_folder_base = Path('../images/hole_cutouts/') + self.scad_runner.camera_arguments = CameraArguments(Vec3(0,0,0), CameraRotations.AngledTop, 50) def test_refined_hole(self): """ - refined_hole() is special, since top_angle_camera is not appropriate for it. + refined_hole() is special, since top_angle_camera is not appropriate for it. """ - camera_args = CameraArguments(Vec3(0,0,0),Vec3(225,0,225),50) + self.scad_runner.camera_arguments = self.scad_runner.camera_arguments.with_rotation(CameraRotations.AngledBottom) test_args = set_variable_argument('test_options', 'bundle_hole_options(refined_hole=true, magnet_hole=false, screw_hole=false, crush_ribs=false, chamfer=false, supportless=false)') - self.scad_runner.create_image(camera_args, test_args, Path('refined_hole.png')) + self.scad_runner.create_image(test_args, Path('refined_hole.png')) def test_plain_magnet_hole(self): test_args = set_variable_argument('test_options', 'bundle_hole_options(refined_hole=false, magnet_hole=true, screw_hole=false, crush_ribs=false, chamfer=false, supportless=false)') - self.scad_runner.create_image(self.scad_runner.TOP_ANGLE_CAMERA, - test_args, Path('magnet_hole.png')) + self.scad_runner.create_image(test_args, Path('magnet_hole.png')) def test_plain_screw_hole(self): test_args = set_variable_argument('test_options', 'bundle_hole_options(refined_hole=false, magnet_hole=false, screw_hole=true, crush_ribs=false, chamfer=false, supportless=false)') - self.scad_runner.create_image(self.scad_runner.TOP_ANGLE_CAMERA, - test_args, Path('screw_hole.png')) + self.scad_runner.create_image(test_args, Path('screw_hole.png')) def test_magnet_and_screw_hole(self): test_args = set_variable_argument('test_options', 'bundle_hole_options(refined_hole=false, magnet_hole=true, screw_hole=true, crush_ribs=false, chamfer=false, supportless=false)') - self.scad_runner.create_image(self.scad_runner.TOP_ANGLE_CAMERA, - test_args, Path('magnet_and_screw_hole.png')) + self.scad_runner.create_image(test_args, Path('magnet_and_screw_hole.png')) def test_chamfered_magnet_hole(self): test_args = set_variable_argument('test_options', 'bundle_hole_options(refined_hole=false, magnet_hole=true, screw_hole=false, crush_ribs=false, chamfer=true, supportless=false)') - self.scad_runner.create_image(self.scad_runner.TOP_ANGLE_CAMERA, - test_args, Path('chamfered_magnet_hole.png')) + self.scad_runner.create_image(test_args, Path('chamfered_magnet_hole.png')) def test_magnet_hole_crush_ribs(self): test_args = set_variable_argument('test_options', 'bundle_hole_options(refined_hole=false, magnet_hole=true, screw_hole=false, crush_ribs=true, chamfer=false, supportless=false)') - self.scad_runner.create_image(self.scad_runner.TOP_ANGLE_CAMERA, - test_args, Path('magnet_hole_crush_ribs.png')) + self.scad_runner.create_image(test_args, Path('magnet_hole_crush_ribs.png')) def test_magnet_hole_supportless(self): test_args = set_variable_argument('test_options', 'bundle_hole_options(refined_hole=false, magnet_hole=true, screw_hole=false, crush_ribs=false, chamfer=false, supportless=true)') - self.scad_runner.create_image(self.scad_runner.TOP_ANGLE_CAMERA, - test_args, Path('magnet_hole_supportless.png')) + self.scad_runner.create_image(test_args, Path('magnet_hole_supportless.png')) def test_magnet_and_screw_hole_supportless(self): test_args = set_variable_argument('test_options', 'bundle_hole_options(refined_hole=false, magnet_hole=true, screw_hole=true, crush_ribs=false, chamfer=false, supportless=true)') - self.scad_runner.create_image(self.scad_runner.TOP_ANGLE_CAMERA, - test_args, Path('magnet_and_screw_hole_supportless.png')) + self.scad_runner.create_image(test_args, Path('magnet_and_screw_hole_supportless.png')) def test_all_hole_options(self): test_args = set_variable_argument('test_options', 'bundle_hole_options(refined_hole=false, magnet_hole=true, screw_hole=true, crush_ribs=true, chamfer=true, supportless=true)') - self.scad_runner.create_image(self.scad_runner.TOP_ANGLE_CAMERA, - test_args, Path('all_hole_options.png')) + self.scad_runner.create_image(test_args, Path('all_hole_options.png')) def test_no_hole(self): test_args = set_variable_argument('test_options', 'bundle_hole_options(refined_hole=false, magnet_hole=false, screw_hole=false, crush_ribs=true, chamfer=true, supportless=true)') - self.scad_runner.create_image(self.scad_runner.TOP_ANGLE_CAMERA, - test_args, Path('no_hole.png')) + self.scad_runner.create_image(test_args, Path('no_hole.png')) if __name__ == '__main__': unittest.main() diff --git a/tests/test_spiral_vase.py b/tests/test_spiral_vase.py index 386d457..6c58e57 100644 --- a/tests/test_spiral_vase.py +++ b/tests/test_spiral_vase.py @@ -23,27 +23,27 @@ class TestSpiralVaseBase(unittest.TestCase): parameter_file_path = Path("gridfinity-spiral-vase.json") parameter_file_data = ParameterFile.from_json(parameter_file_path.read_text()) cls.default_parameters = parameter_file_data.parameterSets["Default"] - cls.camera_args = CameraArguments(Vec3(0,0,0),Vec3(225,0,225),150) def setUp(self): self.scad_runner = OpenScadRunner(Path('../gridfinity-spiral-vase.scad')) self.scad_runner.image_folder_base = Path('../images/spiral_vase_base/') self.scad_runner.parameters = self.default_parameters.copy() self.scad_runner.parameters["type"] = 1 # Create a Base + self.scad_runner.camera_arguments = CameraArguments(Vec3(0,0,0), CameraRotations.AngledBottom, 150) def test_no_holes(self): vars = self.scad_runner.parameters vars["enable_holes"] = False - self.scad_runner.create_image(self.camera_args, [], Path('no_holes_bottom.png')) - self.camera_args = CameraArguments(Vec3(0,0,0),Vec3(45,0,0),150) - self.scad_runner.create_image(self.camera_args, [], Path('no_holes_top.png')) + self.scad_runner.create_image([], Path('no_holes_bottom.png')) + self.scad_runner.camera_arguments = self.scad_runner.camera_arguments.with_rotation(CameraRotations.Top) + self.scad_runner.create_image([], Path('no_holes_top.png')) def test_refined_holes(self): vars = self.scad_runner.parameters vars["enable_holes"] = True - self.scad_runner.create_image(self.camera_args, [], Path('with_holes_bottom.png')) - self.camera_args = CameraArguments(Vec3(0,0,0),Vec3(45,0,0),150) - self.scad_runner.create_image(self.camera_args, [], Path('with_holes_top.png')) + self.scad_runner.create_image([], Path('with_holes_bottom.png')) + self.scad_runner.camera_arguments = self.scad_runner.camera_arguments.with_rotation(CameraRotations.Top) + self.scad_runner.create_image([], Path('with_holes_top.png')) if __name__ == '__main__':