mirror of
https://github.com/kennetek/gridfinity-rebuilt-openscad.git
synced 2024-11-17 22:10:50 +00:00
Add a test suite to generate images of bins with holes
Used to ensure all hole options work.
This commit is contained in:
parent
447df8621b
commit
9b89773812
5 changed files with 251 additions and 9 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,11 +1,9 @@
|
||||||
|
|
||||||
ignore/
|
ignore/
|
||||||
gridfinity-rebuilt.json
|
|
||||||
gridfinity-rebuilt-bins.json
|
|
||||||
stl/
|
stl/
|
||||||
batch/
|
batch/
|
||||||
site/
|
site/
|
||||||
*.json
|
/*.json
|
||||||
|
|
||||||
# From https://github.com/github/gitignore/blob/main/Python.gitignore
|
# From https://github.com/github/gitignore/blob/main/Python.gitignore
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
|
|
36
tests/gridfinity-rebuilt-bins.json
Normal file
36
tests/gridfinity-rebuilt-bins.json
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"fileFormatVersion": "1",
|
||||||
|
"parameterSets": {
|
||||||
|
"Default": {
|
||||||
|
"$fa": "8",
|
||||||
|
"$fs": "0.25",
|
||||||
|
"c_chamfer": "0.5",
|
||||||
|
"c_depth": "1",
|
||||||
|
"c_orientation": "2",
|
||||||
|
"cd": "10",
|
||||||
|
"cdivx": "0",
|
||||||
|
"cdivy": "0",
|
||||||
|
"ch": "1",
|
||||||
|
"chamfer_magnet_holes": "true",
|
||||||
|
"crush_ribs": "true",
|
||||||
|
"div_base_x": "0",
|
||||||
|
"div_base_y": "0",
|
||||||
|
"divx": "0",
|
||||||
|
"divy": "0",
|
||||||
|
"enable_zsnap": "false",
|
||||||
|
"gridx": "1",
|
||||||
|
"gridy": "1",
|
||||||
|
"gridz": "6",
|
||||||
|
"gridz_define": "0",
|
||||||
|
"height_internal": "0",
|
||||||
|
"magnet_holes": "false",
|
||||||
|
"only_corners": "false",
|
||||||
|
"printable_hole_top": "true",
|
||||||
|
"refined_hole": "true",
|
||||||
|
"scoop": "0",
|
||||||
|
"screw_holes": "false",
|
||||||
|
"style_lip": "0",
|
||||||
|
"style_tab": "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,9 +3,20 @@ Helpful classes for running OpenScad from Python.
|
||||||
@Copyright Arthur Moore 2024 MIT License
|
@Copyright Arthur Moore 2024 MIT License
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from dataclasses import dataclass, is_dataclass, asdict
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import NamedTuple
|
from tempfile import NamedTemporaryFile
|
||||||
|
from typing import NamedTuple, Optional
|
||||||
|
|
||||||
|
class DataClassJSONEncoder(json.JSONEncoder):
|
||||||
|
'''Allow json serialization'''
|
||||||
|
def default(self, o):
|
||||||
|
if is_dataclass(o):
|
||||||
|
return asdict(o)
|
||||||
|
# Let the base class default method raise the TypeError
|
||||||
|
return super().default(o)
|
||||||
|
|
||||||
class Vec3(NamedTuple):
|
class Vec3(NamedTuple):
|
||||||
'''Simple 3d Vector (x, y, z)'''
|
'''Simple 3d Vector (x, y, z)'''
|
||||||
|
@ -13,7 +24,8 @@ class Vec3(NamedTuple):
|
||||||
y: float
|
y: float
|
||||||
z: float
|
z: float
|
||||||
|
|
||||||
class CameraArguments(NamedTuple):
|
@dataclass
|
||||||
|
class CameraArguments:
|
||||||
"""
|
"""
|
||||||
Controls the camera position when outputting to png format.
|
Controls the camera position when outputting to png format.
|
||||||
@see `openscad -h`.
|
@see `openscad -h`.
|
||||||
|
@ -26,7 +38,41 @@ class CameraArguments(NamedTuple):
|
||||||
return '--camera=' \
|
return '--camera=' \
|
||||||
f'{",".join(map(str,self.translate))},{",".join(map(str,self.rotate))},{self.distance}'
|
f'{",".join(map(str,self.translate))},{",".join(map(str,self.rotate))},{self.distance}'
|
||||||
|
|
||||||
def set_variable_argument(var: str, val) -> [str, str]:
|
@dataclass(kw_only=True, frozen=True)
|
||||||
|
class ParameterFile:
|
||||||
|
parameterSets: dict[str, dict]
|
||||||
|
fileFormatVersion: int = 1
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(cls, *pargs, **nargs):
|
||||||
|
"""
|
||||||
|
Wrapper for `json.loads`, with some post-processing.
|
||||||
|
The Customizer saves everything as strings. --Arthur 2024-04-28
|
||||||
|
"""
|
||||||
|
nargs["object_pairs_hook"] = cls.object_pairs_hook
|
||||||
|
file = ParameterFile(**json.loads(*pargs, **nargs))
|
||||||
|
assert(file.fileFormatVersion == 1)
|
||||||
|
return file
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def object_pairs_hook(self, pairs: list[tuple]):
|
||||||
|
'''Fixes customizer turning everything into strings'''
|
||||||
|
output = dict(pairs)
|
||||||
|
for (key, value) in output.items():
|
||||||
|
if(type(value) == str):
|
||||||
|
if(value == "true"):
|
||||||
|
output[key] = True
|
||||||
|
continue
|
||||||
|
if(value == "false"):
|
||||||
|
output[key] = False
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
output[key] = float(value)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return output
|
||||||
|
|
||||||
|
def set_variable_argument(var: str, val: str) -> [str, str]:
|
||||||
"""
|
"""
|
||||||
Allows setting a variable to a particular value.
|
Allows setting a variable to a particular value.
|
||||||
@warning value **can** be a function, but this is called for every file, so may generate 'undefined' warnings.
|
@warning value **can** be a function, but this is called for every file, so may generate 'undefined' warnings.
|
||||||
|
@ -38,8 +84,10 @@ class OpenScadRunner:
|
||||||
scad_file_path: Path
|
scad_file_path: Path
|
||||||
openscad_binary_path: Path
|
openscad_binary_path: Path
|
||||||
image_folder_base: Path
|
image_folder_base: Path
|
||||||
|
parameters: Optional[dict]
|
||||||
|
'''If set, a temporary parameter file is created, and used with these variables'''
|
||||||
|
|
||||||
WINDOWS_DEFAULT_PATH = 'C:\Program Files\OpenSCAD\openscad.exe'
|
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),50)
|
||||||
|
|
||||||
common_arguments = [
|
common_arguments = [
|
||||||
|
@ -49,6 +97,8 @@ class OpenScadRunner:
|
||||||
'--imgsize=1280,720',
|
'--imgsize=1280,720',
|
||||||
'--view=axes',
|
'--view=axes',
|
||||||
'--projection=ortho',
|
'--projection=ortho',
|
||||||
|
#"--summary", "all",
|
||||||
|
#"--summary-file", "-"
|
||||||
] + \
|
] + \
|
||||||
set_variable_argument('$fa', 8) + set_variable_argument('$fs', 0.25)
|
set_variable_argument('$fa', 8) + set_variable_argument('$fs', 0.25)
|
||||||
|
|
||||||
|
@ -56,6 +106,7 @@ class OpenScadRunner:
|
||||||
self.openscad_binary_path = self.WINDOWS_DEFAULT_PATH
|
self.openscad_binary_path = self.WINDOWS_DEFAULT_PATH
|
||||||
self.scad_file_path = file_path
|
self.scad_file_path = file_path
|
||||||
self.image_folder_base = Path('.')
|
self.image_folder_base = Path('.')
|
||||||
|
self.parameters = None
|
||||||
|
|
||||||
def create_image(self, camera_args: CameraArguments, args: [str], image_file_name: str):
|
def create_image(self, camera_args: CameraArguments, args: [str], image_file_name: str):
|
||||||
"""
|
"""
|
||||||
|
@ -69,6 +120,15 @@ class OpenScadRunner:
|
||||||
image_path = self.image_folder_base.joinpath(image_file_name)
|
image_path = self.image_folder_base.joinpath(image_file_name)
|
||||||
command_arguments = self.common_arguments + \
|
command_arguments = self.common_arguments + \
|
||||||
[camera_args.as_argument()] + args + \
|
[camera_args.as_argument()] + args + \
|
||||||
[f'-o{str(image_path)}', str(self.scad_file_path)]
|
["-o", str(image_path), str(self.scad_file_path)]
|
||||||
#print(command_arguments)
|
#print(command_arguments)
|
||||||
|
|
||||||
|
if self.parameters != None:
|
||||||
|
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)
|
||||||
|
file.close()
|
||||||
|
command_arguments += ["-p", file.name, "-P", "python_generated"]
|
||||||
|
return subprocess.run([self.openscad_binary_path]+command_arguments, check=True)
|
||||||
|
else:
|
||||||
return subprocess.run([self.openscad_binary_path]+command_arguments, check=True)
|
return subprocess.run([self.openscad_binary_path]+command_arguments, check=True)
|
||||||
|
|
142
tests/test_bins.py
Normal file
142
tests/test_bins.py
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
"""
|
||||||
|
Tests for gridfinity-rebuilt-bins.scad
|
||||||
|
@Copyright Arthur Moore 2024 MIT License
|
||||||
|
"""
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
|
import json
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from tempfile import NamedTemporaryFile
|
||||||
|
|
||||||
|
from openscad_runner import *
|
||||||
|
|
||||||
|
class TestBinHoles(unittest.TestCase):
|
||||||
|
"""
|
||||||
|
Test how a single base looks with holes cut out.
|
||||||
|
|
||||||
|
Currently only makes sure code runs, and outputs pictures for manual verification.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
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()
|
||||||
|
|
||||||
|
def test_no_holes(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["refined_hole"] = False
|
||||||
|
vars["magnet_hole"] = False
|
||||||
|
vars["screw_hole"] = False
|
||||||
|
self.scad_runner.create_image(self.camera_args, [], Path('no_holes.png'))
|
||||||
|
|
||||||
|
def test_refined_holes(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["refined_hole"] = True
|
||||||
|
vars["magnet_hole"] = False
|
||||||
|
vars["screw_hole"] = False
|
||||||
|
self.scad_runner.create_image(self.camera_args, [], Path('refined_holes.png'))
|
||||||
|
|
||||||
|
def test_refined_and_screw_holes(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["refined_hole"] = True
|
||||||
|
vars["magnet_hole"] = False
|
||||||
|
vars["screw_hole"] = True
|
||||||
|
vars["printable_hole_top"] = False
|
||||||
|
self.scad_runner.create_image(self.camera_args, [], Path('refined_and_screw_holes.png'))
|
||||||
|
|
||||||
|
def test_screw_holes_plain(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["refined_hole"] = False
|
||||||
|
vars["magnet_hole"] = False
|
||||||
|
vars["screw_hole"] = True
|
||||||
|
vars["printable_hole_top"] = False
|
||||||
|
self.scad_runner.create_image(self.camera_args, [], Path('screw_holes_plain.png'))
|
||||||
|
|
||||||
|
def test_screw_holes_printable(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["refined_hole"] = False
|
||||||
|
vars["magnet_hole"] = False
|
||||||
|
vars["screw_hole"] = True
|
||||||
|
vars["printable_hole_top"] = True
|
||||||
|
self.scad_runner.create_image(self.camera_args, [], Path('screw_holes_printable.png'))
|
||||||
|
|
||||||
|
def test_magnet_holes_plain(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["refined_hole"] = False
|
||||||
|
vars["magnet_hole"] = True
|
||||||
|
vars["screw_hole"] = False
|
||||||
|
vars["crush_ribs"] = False
|
||||||
|
vars["chamfer_magnet_holes"] = False
|
||||||
|
vars["printable_hole_top"] = False
|
||||||
|
self.scad_runner.create_image(self.camera_args, [], Path('magnet_holes_plain.png'))
|
||||||
|
|
||||||
|
def test_magnet_holes_chamfered(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["refined_hole"] = False
|
||||||
|
vars["magnet_hole"] = True
|
||||||
|
vars["screw_hole"] = False
|
||||||
|
vars["crush_ribs"] = False
|
||||||
|
vars["chamfer_magnet_holes"] = True
|
||||||
|
vars["printable_hole_top"] = False
|
||||||
|
self.scad_runner.create_image(self.camera_args, [], Path('magnet_holes_chamfered.png'))
|
||||||
|
|
||||||
|
def test_magnet_holes_printable(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["refined_hole"] = False
|
||||||
|
vars["magnet_hole"] = True
|
||||||
|
vars["screw_hole"] = False
|
||||||
|
vars["crush_ribs"] = False
|
||||||
|
vars["chamfer_magnet_holes"] = False
|
||||||
|
vars["printable_hole_top"] = True
|
||||||
|
self.scad_runner.create_image(self.camera_args, [], Path('magnet_holes_printable.png'))
|
||||||
|
|
||||||
|
def test_magnet_holes_with_crush_ribs(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["refined_hole"] = False
|
||||||
|
vars["magnet_hole"] = True
|
||||||
|
vars["screw_hole"] = False
|
||||||
|
vars["crush_ribs"] = True
|
||||||
|
vars["chamfer_magnet_holes"] = False
|
||||||
|
vars["printable_hole_top"] = False
|
||||||
|
self.scad_runner.create_image(self.camera_args, [], Path('magnet_holes_with_crush_ribs.png'))
|
||||||
|
|
||||||
|
def test_magnet_and_screw_holes_plain(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["refined_hole"] = False
|
||||||
|
vars["magnet_hole"] = True
|
||||||
|
vars["screw_hole"] = True
|
||||||
|
vars["crush_ribs"] = False
|
||||||
|
vars["chamfer_magnet_holes"] = False
|
||||||
|
vars["printable_hole_top"] = False
|
||||||
|
self.scad_runner.create_image(self.camera_args, [], Path('magnet_and_screw_holes_plain.png'))
|
||||||
|
|
||||||
|
def test_magnet_and_screw_holes_printable(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["refined_hole"] = False
|
||||||
|
vars["magnet_hole"] = True
|
||||||
|
vars["screw_hole"] = True
|
||||||
|
vars["crush_ribs"] = False
|
||||||
|
vars["chamfer_magnet_holes"] = False
|
||||||
|
vars["printable_hole_top"] = True
|
||||||
|
self.scad_runner.create_image(self.camera_args, [], Path('magnet_and_screw_holes_printable.png'))
|
||||||
|
|
||||||
|
def test_magnet_and_screw_holes_all(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["refined_hole"] = False
|
||||||
|
vars["magnet_hole"] = True
|
||||||
|
vars["screw_hole"] = True
|
||||||
|
vars["crush_ribs"] = True
|
||||||
|
vars["chamfer_magnet_holes"] = True
|
||||||
|
vars["printable_hole_top"] = True
|
||||||
|
self.scad_runner.create_image(self.camera_args, [], Path('magnet_and_screw_holes_all.png'))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
|
@ -77,5 +77,11 @@ class TestHoleCutouts(unittest.TestCase):
|
||||||
self.scad_runner.create_image(self.scad_runner.TOP_ANGLE_CAMERA,
|
self.scad_runner.create_image(self.scad_runner.TOP_ANGLE_CAMERA,
|
||||||
test_args, Path('all_hole_options.png'))
|
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'))
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
Loading…
Reference in a new issue