Skip to content

Commit

Permalink
OpenScad Python Runner - Make camera_args a variable
Browse files Browse the repository at this point in the history
As opposed to being a parameter of `create_image`.

Also made CameraArguments support fluent.
  • Loading branch information
EmperorArthur committed Apr 30, 2024
1 parent cd56b0e commit a4807fd
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 50 deletions.
31 changes: 26 additions & 5 deletions tests/openscad_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}'

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -106,9 +124,10 @@ def __init__(self, file_path: Path):
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.
Expand All @@ -119,11 +138,13 @@ def create_image(self, camera_args: CameraArguments, args: [str], image_file_nam

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)
Expand Down
26 changes: 13 additions & 13 deletions tests/test_bins.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,50 +23,50 @@ def setUpClass(cls):
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
vars["refined_holes"] = True
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
vars["refined_holes"] = False
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
vars["refined_holes"] = False
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
Expand All @@ -76,7 +76,7 @@ def test_magnet_holes_plain(self):
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
Expand All @@ -86,7 +86,7 @@ def test_magnet_holes_chamfered(self):
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
Expand All @@ -96,7 +96,7 @@ def test_magnet_holes_printable(self):
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
Expand All @@ -106,7 +106,7 @@ def test_magnet_holes_with_crush_ribs(self):
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
Expand All @@ -116,7 +116,7 @@ def test_magnet_and_screw_holes_plain(self):
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
Expand All @@ -126,7 +126,7 @@ def test_magnet_and_screw_holes_printable(self):
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
Expand All @@ -136,7 +136,7 @@ def test_magnet_and_screw_holes_all(self):
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()
41 changes: 16 additions & 25 deletions tests/test_holes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
14 changes: 7 additions & 7 deletions tests/test_spiral_vase.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,27 @@ def setUpClass(cls):
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__':
Expand Down

0 comments on commit a4807fd

Please sign in to comment.