-
Notifications
You must be signed in to change notification settings - Fork 11
/
OpenScadInterface.py
218 lines (151 loc) · 7.85 KB
/
OpenScadInterface.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
import os
import platform
import shutil
import subprocess
from UM.Logger import Logger
from UM.Message import Message
class OpenScadInterface:
_openscad_version_id = 'OpenSCAD version '
def __init__(self, pluginName, tempDir):
self.errorMessage = ''
self._openScadPath = ''
self._pluginName = pluginName
self._openscad_version = ''
self._tempDir = tempDir
def SetOpenScadPath(self, openScadPath):
''' Manually assign the OpenScad path '''
self._openScadPath = openScadPath
@property
def OpenScadVersion(self):
return self._openscad_version
@property
def OpenScadPath(self)->str:
''' Return the path to OpenScad - an attempt will be made to automatically determine it if needed '''
if self._openScadPath == '' or self._openScadPath == 'openscad':
self._openScadPath = self._GetDefaultOpenScadPath()
Logger.log('e', f'default openscad path is "{self._openScadPath}"')
return self._openScadPath
@property
def OpenScadPathValid(self)->bool:
''' Return true if the OpenScad path is valid '''
# Attempt to verify the OpenScad executable is valid by querying the OpenScad version number
command = f'{self._OpenScadCommand} -v'
response = subprocess.run(command, capture_output=True, text=True, shell=True).stderr.strip()
Logger.log('d', f'Checking for OpenSCAD returned the following response: "{response}"')
# OpenScad is considered valid if it returns a response including the string 'OpenScad version'
valid = self._openscad_version_id in response
if valid:
self._openscad_version = response.replace(self._openscad_version_id, '')
Logger.log('d', 'The OpenSCAD path is valid')
else:
self._openscad_version = ''
Logger.log('d', 'The OpenSCAD path is not valid')
return valid
@property
def _OpenScadCommand(self)->str:
''' Converts the OpenScad path into a form that can be executed
Currently, this is only needed for Linux '''
command = ''
# This only makes sense if the OpenScad path has been determined or set
if self.OpenScadPath != '':
# If running on Linux as an AppImage, the LD_LIBRARY_PATH environmental variable can cause issues
# In order for OpenScad to run correctly in this case, LD_LIBRARY_PATH needs to be unset
# At least on my machine...
system = platform.system().lower()
if system == 'linux':
# Prefix the OpenScad call with a command to unset LD_LIBRARY_PATH
command += 'unset LD_LIBRARY_PATH; '
path = self.OpenScadPath
# Add the executable to the command
# Quotes are added in case there are embedded spaces in the path, but we shouldn't double up if already quoted
command += f'"{path}"' if not path.startswith('\"') else path
return command
def GenerateStl(self, inputFilePath, parameters, outputFilePath):
'''Execute an OpenSCAD file with the given parameters to generate a model'''
# If the OpenScad path is valid
if self.OpenScadPathValid:
# Build the OpenSCAD command
command = self._GenerateOpenScadCommand(inputFilePath, parameters, outputFilePath)
Logger.log('d', f'Executing OpenSCAD command: {command}')
# Execute the OpenSCAD command and capture the error output
# Output in stderr does not necessarily indicate an error - OpenSCAD seems to routinely output to stderr
try:
self.commandResult = subprocess.run(command, capture_output=True, text=True, shell=True).stderr.strip()
except FileNotFoundError:
Message(f'OpenSCAD was not found at path "{self._openScadPath}"', title=self._pluginName, message_type=Message.MessageType.ERROR).show()
# If the OpenScad path is invalid
else:
Message(f'The OpenSCAD path is invalid', title=self._pluginName, message_type=Message.MessageType.ERROR).show()
def _GenerateOpenScadCommand(self, inputFilePath, parameters, outputFilePath):
'''Generate an OpenSCAD command from an input file path, parameters, and output file path'''
# Start the command line
command_line = self._OpenScadCommand
# Tell OpenSCAD to automatically generate an STL file
command_line += f' -o "{outputFilePath}"'
# Add each variable setting parameter
for parameter in parameters:
# Retrieve the parameter value
value = parameters[parameter]
# If the value is a string, add quotes around it
if type(value) == str:
value = f'\\"{value}\\"'
command_line += f' -D "{parameter}={value}"'
# Finally, specify the OpenSCAD source file
inputFilePath = self._SymLinkWorkAround(inputFilePath)
command_line += f' "{inputFilePath}"'
return command_line
def _GetDefaultOpenScadPath(self):
''' Attempt to determine the default location of the OpenScad executable '''
# This makes a sensible default
openScadPath = 'openscad'
# Determine the system that Cura is being run on
system = platform.system().lower()
# On Linux, check for openscad in the current path
if system == "linux":
# If the 'which' command can find the openscad path, use the path directly
command = 'which openscad'
which_result = subprocess.run(command, capture_output=True, text=True, shell=True).stdout.strip()
if which_result != '':
openScadPath = which_result
# This path for Macintosh was borrowed from Thopiekar's OpenSCAD Integration plugin (https://thopiekar.eu/cura/cad/openscad)
# I have no way of verifying it works...
if system == 'darwin':
openScadPath = '/Applications/OpenSCAD.app/Contents/MacOS/OpenSCAD'
# For Windows, OpenSCAD should be installed in one of the Program Files folder
elif system == 'windows':
program_files_paths = (
os.getenv("PROGRAMFILES"),
os.getenv("PROGRAMFILES(X86)")
)
for program_files_path in program_files_paths:
testPath = os.path.join(program_files_path, "OpenSCAD", "openscad.exe")
if os.path.isfile(testPath):
openScadPath = testPath
break
return openScadPath
def _SymLinkWorkAround(self, filePath)->str:
''' OpenSCAD does not appear to handle files with a symlink directory anywhere in its path
This method checks for this condition and copies the provided file to a temprary location, if needed
The original path is returned if a symlink is involved, otherwise the path to the temporary file is returned '''
returnPath = filePath
# Determine if any element in the file path is a symlink
pathCopy = os.path.normpath(filePath)
isSymLink = False
while True:
# Check if the current portion of the path is a sym link
if os.path.islink(pathCopy):
isSymLink = True
break
# Throw away the last portion of the path and check again
split = os.path.split(pathCopy)
if pathCopy == split[0]:
break
pathCopy = split[0]
# If the file path involves a sym link, copy it to a temporary file
if isSymLink:
# Copy the file to the system's temporary directory
fileName = os.path.basename(filePath)
tempFilePath = os.path.join(self._tempDir, fileName)
shutil.copy2(filePath, tempFilePath)
returnPath = tempFilePath
return returnPath