OpenScad Python Runner - Make camera_args a variable

As opposed to being a parameter of `create_image`.

Also made CameraArguments support fluent.
This commit is contained in:
Arthur Moore 2024-04-30 07:43:09 -04:00
parent cd56b0e421
commit a4807fd3ab
4 changed files with 62 additions and 50 deletions

View file

@ -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)

View file

@ -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()

View file

@ -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()

View file

@ -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__':