Read time: 2.6 minutes (256 words)
Step 6: OpenSCAD Manager¶
Most of the analysis of a design will be done using an STL file which describes a part we create. Before we worry about generating the parts, we can set up a Python class that will manage access to OpenSCAD from the command line.
Feature 2: Add OpenSCAD Management class¶
The primary purpose of the OpenSCAD manager class is to isolate running OpenSCAD to perform needed operations. For our first test of this class, we will launch OpenSCAD and fetch the version of the program we are using. Unfortunately, getting the Continuous Integration servers to install the version I have on my machine (installed with Homebrew) has proven difficult, so we will not test the actual version, just ensure we get valid output.
The code we need to write will assume that OpenSCAD is on the system path and can be launched with the name openscad.
Test OpenSCAD Version¶
Users will need to install at least OpenSCAD version 2019.05, so we will set up a test to check that.
Here is our first test for this class:
1import os
2import json
3from mmdesigner.OpenSCAD import OpenSCAD
4
5
6def test_OSC_version():
7 """Test app returns installed OpenSCAD version"""
8 mgr = OpenSCAD()
9 version = mgr.get_version()
10 year, rel = version.split(".")
11 assert int(year) >= 2019
12
13
14def test_OSC_STL_generator():
15 """Test generation of STL file from specified SCAD file"""
16 mgr = OpenSCAD()
17 scad_file = "tests/test_data/spar.scad"
18 stl_file = "tests/test_data/spar.stl"
19 if os.path.exists(stl_file):
20 os.remove(stl_file)
21 mgr.gen_stl(scad_file)
22 assert os.path.isfile(stl_file)
23
24
25def test_OSC_STL_bad_file():
26 """Test error return on bad file name"""
27 mgr = OpenSCAD()
28 assert mgr.gen_stl("bad") == 1
29
30
31def test_OSC_STL_bad_stl_name():
32 """Test error return on non-scad file name"""
33 stl_file = "tests/test_data/spar.stl"
34 mgr = OpenSCAD()
35 assert mgr.gen_stl(stl_file) == 1
36
37
38def test_mass_properties():
39 mgr = OpenSCAD()
40 scad_file = "tests/test_data/spar.scad"
41 err_code = mgr.get_properties(scad_file)
42 assert err_code == 0
43 assert mgr.get_bounds() == [1.0, 0.0, 5.0, 0.0, 1.0, 0.0]
44
45
46def test_OSC_json():
47 mgr = OpenSCAD()
48 scad_file = "tests/test_data/spar.scad"
49 json_file = "tests/test_data/spar.json"
50 err_code = mgr.get_properties(scad_file)
51 assert err_code == 0
52 mgr.dump_to_json(json_file)
53 with open(json_file) as jin:
54 jdata = json.load(jin)
55 assert float(jdata["maxy"]) == 5.0
And here is the start of our class:
1import os
2import stl
3from stl import mesh
4import subprocess
5
6
7class OpenSCAD(object):
8 """Management class for OpenSCAD command line interface"""
9 def __init__(self):
10 pass
11
12 def get_version(self):
13 """return installed OpenScAD version"""
With this running locally, we now need to modify the Continuous Integration service control files and get OpenSCAD installed on their virtual machines for testing. This took some research, but i finally managed to get things working.
Test OpenSCAD STL Generation¶
Our next task involves getting OpenSCAD to generate an STL file from the associated SCAD file.
1 result = subprocess.run(['openscad', '--version'], capture_output=True)
2 version = result.stderr.decode().split()[2]
3 return version
4
5 def gen_stl(self, scad_path):
6 """ Generate STL file from specified SCAD file"""
7 print("Generating STL file from ", scad_path)
8 err_code = 1
9 if not os.path.isfile(scad_path):
10 return err_code
11 if not scad_path.endswith(".scad"):
12 return err_code
13 base, ext = scad_path.split(".")
14 stl_path = base+".stl"
15 cmd = [
16 'openscad',
17 scad_path,
18 "-o",
19 stl_path
Mass Property Generator¶
Now that we can generate an STL file, we can analyze that file to get the part properties. These will be saved in a JSON output file:
1import os
2import stl
3from stl import mesh
4import subprocess
5
6
7class OpenSCAD(object):
8 """Management class for OpenSCAD command line interface"""
9 def __init__(self):
10 pass
11
12 def get_version(self):
13 """return installed OpenScAD version"""
14 result = subprocess.run(['openscad', '--version'], capture_output=True)
15 version = result.stderr.decode().split()[2]
16 return version
17
18 def gen_stl(self, scad_path):
19 """ Generate STL file from specified SCAD file"""
20 print("Generating STL file from ", scad_path)
21 err_code = 1
22 if not os.path.isfile(scad_path):
23 return err_code
24 if not scad_path.endswith(".scad"):
25 return err_code
26 base, ext = scad_path.split(".")
27 stl_path = base+".stl"
28 cmd = [
29 'openscad',
30 scad_path,
31 "-o",
32 stl_path
33 ]
34
35 result = subprocess.run(cmd, capture_output=True)
36 err_code = result.returncode
37 return err_code
38
39 def _get_bounds(self):
40 """set internal bounds variables"""
41 minx = maxx = miny = maxy = minz = maxz = None
42 obj = self.mesh
43 for p in obj.points:
44 if minx is None:
45 minx = p[stl.Dimension.X]
46 maxx = p[stl.Dimension.X]
47 miny = p[stl.Dimension.Y]
48 maxy = p[stl.Dimension.Y]
49 minz = p[stl.Dimension.Z]
50 maxz = p[stl.Dimension.Z]
51 else:
52 maxx = max(p[stl.Dimension.X], maxx)
53 minx = min(p[stl.Dimension.X], minx)
54 maxy = max(p[stl.Dimension.Y], maxy)
55 miny = min(p[stl.Dimension.Y], miny)
56 maxz = max(p[stl.Dimension.Z], maxz)
57 minz = min(p[stl.Dimension.Z], minz)
58 self.minx = minx
59 self.maxx = maxx
60 self.miny = miny
61 self.maxy = maxy
62 self.minz = minz
63 self.maxz = maxz
64
65 def dump_to_json(self, json_path):
66 """write mas properties data to specified JSON file"""
67 json = "{\n"
68 json += ' "maxx": "%s",\n' % self.maxx
69 json += ' "minx": "%s",\n' % self.minx
70 json += ' "maxy": "%s",\n' % self.maxy
71 json += ' "miny": "%s",\n' % self.miny
72 json += ' "maxz": "%s",\n' % self.maxz
73 json += ' "minz": "%s",\n' % self.minz
74 json += ' "volume": "%s",\n' % self.volume
75 json += ' "cgx": "%s",\n' % self.cgx
76 json += ' "cgy": "%s",\n' % self.cgy
77 json += ' "cgz": "%s",\n' % self.cgz
78 json += ' "ixx": "%s",\n' % self.ixx
79 json += ' "ixy": "%s",\n' % self.ixy
80 json += ' "ixz": "%s",\n' % self.ixz
81 json += ' "iyx": "%s",\n' % self.iyx
82 json += ' "iyy": "%s",\n' % self.iyy
83 json += ' "iyz": "%s",\n' % self.iyz
84 json += ' "izx": "%s",\n' % self.izx
85 json += ' "izy": "%s",\n' % self.izy
86 json += ' "izz": "%s"\n' % self.izz
87 json += '}\n'
88 with open(json_path, 'w') as fout:
89 fout.write(json)
90
91 def get_properties(self, scad_file):
92 """Calculate mass properties from specified SCAD file"""
93 scad_path = os.path.abspath(scad_file)
94 err_code = self.gen_stl(scad_path)
95 base, ext = scad_path.split(".")
96 stl_file = base + ".stl"
97 self.json_file = base + ".json"
98 self.mesh = mesh.Mesh.from_file(stl_file)
99 self._get_bounds()
100 volume, cog, inertia = self.mesh.get_mass_properties()
101 self.cgx, self.cgy, self.cgz = cog
102 self.volume = volume
103 self.ixx, self.ixy, self.ixz = inertia[0]
104 self.iyx, self.iyy, self.iyz = inertia[1]
105 self.izx, self.izy, self.izz = inertia[2]
106 self.dump_to_json(self.json_file)
107 return err_code
108
109 def get_bounds(self):
110 """Return bounds data as a list"""
111 return [
112 self.maxx, self.minx,
113 self.maxy, self.miny,
114 self.maxz, self.minz
115 ]
116
117
118if __name__ == '__main__':
119 mgr = OpenSCAD()
120 print(mgr.get_version())
121 mgr.get_properties("../tests/test_data/spar.scad")
122 mgr.dump_to_json("../tests/test_data/spar.json")
This code depends on numpy-stl which needs to be added to the requirements.txt file.