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
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
$Id$
Copyright 2010 Lars Kruse <devel@sumpfralle.de>
Copyright 2008-2009 Lode Leroy
This file is part of PyCAM.
PyCAM is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
PyCAM is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
"""
from optparse import OptionParser
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
from pycam.Physics.ode_physics import override_ode_availability
import pycam.Gui.common as GuiCommon
import pycam.Gui.Settings
import pycam.Importers.STLImporter
import pycam.Importers.TestModel
import pycam.Exporters.SimpleGCodeExporter
import pycam.Toolpath.Generator
import pycam.Toolpath
from pycam import VERSION
import pycam.Utils.log
log = pycam.Utils.log.get_logger()
EXAMPLE_MODEL_LOCATIONS = (
os.path.join(os.path.dirname(__file__), "Samples", "STL"),
"/usr/share/pycam/Samples/STL")
DEFAULT_MODEL_FILE = "pycam.stl"
def show_gui(inputfile=None, task_settings_file=None):
deps_gtk = GuiCommon.requirements_details_gtk()
report_gtk = GuiCommon.get_dependency_report(deps_gtk, prefix="\t")
if GuiCommon.check_dependencies(deps_gtk):
from pycam.Gui.Project import ProjectGui
gui_class = ProjectGui
else:
full_report = []
full_report.append("PyCAM dependency problem")
full_report.append("Error: Failed to load the GTK interface.")
full_report.append("Details:")
full_report.append(report_gtk)
full_report.append("")
full_report.append("Detailed list of requirements: %s" % GuiCommon.REQUIREMENTS_LINK)
log.critical(os.linesep.join(full_report))
sys.exit(1)
gui = gui_class()
# load the given model or the default
if inputfile is None:
default_model = get_default_model()
if isinstance(default_model, basestring):
gui.load_model_file(filename=default_model)
else:
gui.load_model(default_model)
else:
gui.load_model_file(filename=inputfile)
# load task settings file
if not task_settings_file is None:
gui.open_task_settings_file(task_settings_file)
# open the GUI
gui.mainloop()
def get_default_model():
""" return a filename or a Model instance """
# try to load the default model file ("pycam" logo)
for inputdir in EXAMPLE_MODEL_LOCATIONS:
inputfile = os.path.join(inputdir, DEFAULT_MODEL_FILE)
if os.path.isfile(inputfile):
return inputfile
else:
# fall back to the simple test model
log.warn("Failed to find the default model (%s) in the " \
"following locations: %s" % (DEFAULT_MODEL_FILE,
", ".join(EXAMPLE_MODEL_LOCATIONS)))
return pycam.Importers.TestModel.get_test_model()
# check if we were started as a separate program
if __name__ == "__main__":
parser = OptionParser( prog="PyCAM", version="PyCAM v%s" % VERSION,
usage="usage: %prog [options] [inputfile]\n\n" \
+ "Use any of the '--export-???' parameters to disable " \
+ "the graphical user interface (GUI).\n" \
+ "When starting the GUI (default) all arguments except " \
+ "'inputfile' are ignored.\n" \
+ "'inputfile' may be an STL or DXF file.",
epilog="Use any of the '--export-???' parameters to enable " \
+ "non-interactive mode. Otherwise all options (except " \
+ "'inputfile') are silently ignored and the GUI is " \
+ "started.")
group_general = parser.add_option_group("General options")
group_export = parser.add_option_group("Export formats",
"Export the resulting toolpath or meta-data in various formats. " \
+ "This option triggers the non-interactive mode. Thus the GUI " \
+ "is disabled.")
group_tool = parser.add_option_group("Tool definition",
"Specify the tool paramters. The default tool is spherical and " \
+ "has a diameter of 1mm. The default speeds are 1000 (feedrate) " \
+ "and 250 (drill spindle rotations per minute)")
group_process = parser.add_option_group("Process definition",
"Specify the process parameters: toolpath strategy, layer height," \
+ " and others. A typical roughing operation is configured by " \
+ "default.")
group_bounds = parser.add_option_group("Boundary definition",
"Specify the outer limits of the processing area (x/y/z). " \
+ "You may choose between 'relative_margin' (margin is given as " \
+ "percentage of the respective model dimension), 'fixed_margin' " \
+ "(margin for each face given in absolute units of length) " \
+ "and 'custom' (absolute coordinates of the bounding box - " \
+ "regardless of the model size and position). Negative values " \
+ "are allowed and can make sense (e.g. negative margin).")
group_support_grid = parser.add_option_group("Support grid",
"An optional support grid can be used to keep the object in " \
+ "place during the mill operation. The support grid can be " \
+ "removed manually afterwards. The support grid can have a " \
+ "rectangular profile. By default the support grid is disabled.")
group_general.add_option("-c", "--config", dest="config_file",
default=None, action="store", type="string",
help="load a task settings file")
group_general.add_option("", "--unit", dest="unit_size",
default="mm", action="store", type="choice",
choices=["mm", "inch"], help="choose 'mm' or 'inch' for all " \
+ "numbers. By default 'mm' is assumed.")
group_general.add_option("", "--collision-engine", dest="collision_engine",
default="triangles", action="store", type="choice",
choices=["triangles", "ode", "help"],
help="choose a specific collision detection engine. The default " \
+ "is 'triangles'. Use 'help' to get a list of possible " \
+ "engines.")
group_general.add_option("", "--boundary-mode", dest="boundary_mode",
default="along", action="store", type="choice",
choices=["inside", "along", "outside"],
help="specify if the mill tool (including its radius) should " \
+ "move completely 'inside', 'along' or 'outside' the " \
+ "defined processing boundary.")
group_export.add_option("", "--export-gcode", dest="export_gcode",
default=None, action="store", type="string",
help="export the generated toolpaths to a file")
group_export.add_option("", "--export-task-config",
dest="export_task_config", default=None, action="store",
type="string",
help="export the current task configuration (mainly for debugging)")
group_tool.add_option("", "--tool-shape", dest="tool_shape",
default="cylindrical", action="store", type="choice",
choices=["cylindrical", "spherical", "toroidal"],
help="tool shape for the operation (cylindrical, spherical, " \
+ "toroidal)")
group_tool.add_option("", "--tool-size", dest="tool_diameter",
default=1.0, action="store", type="float",
help="diameter of the tool")
group_tool.add_option("", "--tool-torus-size", dest="tool_torus_diameter",
default=0.25, action="store", type="float",
help="torus diameter of the tool (only for toroidal tool shape)")
group_tool.add_option("", "--tool-feedrate", dest="tool_feedrate",
default=1000, action="store", type="float",
help="allowed movement velocity of the tool (units/minute)")
group_tool.add_option("", "--tool-speed", dest="tool_speed",
default=250, action="store", type="float",
help="rotation speed of the tool (per minute)")
group_tool.add_option("", "--tool-id", dest="tool_id",
default=1, action="store", type="int",
help="tool ID - to be used for tool selection via GCode " \
+ "(default: 1)")
group_process.add_option("", "--process-path-direction",
dest="process_path_direction", default="x", action="store",
type="choice", choices=["x", "y", "xy"],
help="primary direction of the generated toolpath (x/y/xy)")
group_process.add_option("", "--process-path-generator",
dest="process_path_generator", default="layer", action="store",
type="choice", choices=["layer", "surface", "engrave"],
help="one of the available toolpath strategies (layer, surface, " \
+ "engrave)")
group_process.add_option("", "--process-path-postprocessor",
dest="process_path_postprocessor", default="polygon",
action="store", type="choice",
choices=["polygon", "contour", "zigzag"], help="one of the " \
+ "available toolpath postprocessors (polygon, zigzag, contour)")
group_process.add_option("", "--process-material-allowance",
dest="process_material_allowance", default=0.0, action="store",
type="float", help="minimum distance between the tool and the " \
+ "object (for rough processing)")
group_process.add_option("", "--process-step-down",
dest="process_step_down", default=3.0, action="store", type="float",
help="the maximum thickness of each processed material layer " \
+ "(only for 'layer' strategy)")
group_process.add_option("", "--process-overlap-percent",
dest="process_overlap_percent", default=0, action="store",
type="int", help="how much should two adjacent parallel " \
+ "toolpaths overlap each other (0..99)")
group_process.add_option("", "--safety-height", dest="safety_height",
default=0.0, action="store", type="float",
help="height for safe re-positioning moves")
group_process.add_option("", "--process-engrave-offset",
dest="process_engrave_offset", default=0.0, action="store",
type="float", help="engrave along the contour of a model with a " \
+ "given distance (only for 'engrave' strategy)")
group_bounds.add_option("", "--bounds-type", dest="bounds_type",
default="relative-margin", action="store", type="choice",
choices=["relative-margin", "fixed-margin", "custom"],
help="type of the boundary definition (relative-margin, " \
+ "fixed-margin, custom)")
group_bounds.add_option("", "--bounds-lower", dest="bounds_lower",
default="", action="store", type="string",
help="comma-separated x/y/z combination of the lower boundary " \
+ "(e.g. '4,4,-0.5')")
group_bounds.add_option("", "--bounds-upper", dest="bounds_upper",
default="", action="store", type="string",
help="comma-separated x/y/z combination of the upper boundary " \
+ "(e.g. '12,5.5,0')")
group_support_grid.add_option("", "--enable-support-grid",
dest="support_grid_enabled", default=False, action="store_true",
help="enable the support grid")
group_support_grid.add_option("", "--support-grid-distance",
dest="support_grid_distance", default=10.0, action="store",
type="float", help="horizontal and vertical distance between two " \
+ "adjacent parallel lines of the support grid pattern")
group_support_grid.add_option("", "--support-grid-height",
dest="support_grid_height", default=2.0, action="store",
type="float", help="height of the support grid profile")
group_support_grid.add_option("", "--support-grid-thickness",
dest="support_grid_thickness", default=0.5, action="store",
type="float", help="width of the support grid profile")
(opts, args) = parser.parse_args()
if len(args) > 0:
inputfile = args[0]
else:
inputfile = None
if not opts.export_gcode and not opts.export_task_config:
show_gui(inputfile, opts.config_file)
else:
# generate toolpath
tps = pycam.Gui.Settings.ToolpathSettings()
tool_shape = {"cylindrical": "CylindricalCutter",
"spherical": "SphericalCutter",
"toroidal": "ToroidalCutter",
}[opts.tool_shape]
tps.set_tool(opts.tool_id, tool_shape, 0.5 * opts.tool_diameter,
0.5 * opts.tool_torus_diameter, opts.tool_speed,
opts.tool_feedrate)
if opts.support_grid_enabled:
tps.set_support_grid(opts.support_grid_distance,
opts.support_grid_thickness, opts.support_grid_height)
if opts.collision_engine == "ode":
tps.set_calculation_backend("ODE")
tps.set_unit_size(opts.unit_size)
path_generator = {"layer": "PushCutter",
"surface": "DropCutter",
"engrave": "EngraveCutter",
}[opts.process_path_generator]
postprocessor = {"zigzag": "ZigZagCutter",
"contour": "ContourCutter",
"polygon": "PolygonCutter",
}[opts.process_path_postprocessor]
tps.set_process_settings(path_generator, postprocessor,
opts.process_path_direction, opts.process_material_allowance,
opts.safety_height, opts.process_overlap_percent / 100.0,
opts.process_step_down, opts.process_engrave_offset)
model = get_model(inputfile)
# calculate the processing boundary
bounds = pycam.Toolpath.Bounds()
# set the bounds type and let the default bounding box match the model
if opts.bounds_type == "relative-margin":
bounds.set_type(pycam.Toolpath.Bounds.TYPE_RELATIVE_MARGIN)
bounds_default_low = (10, 10, 0)
bounds_default_high = (10, 10, 0)
elif opts.bounds_type == "fixed-margin":
bounds.set_type(pycam.Toolpath.Bounds.TYPE_FIXED_MARGIN)
bounds_default_low = (10, 10, 0)
bounds_default_high = (10, 10, 0)
else:
# custom boundary setting
bounds.set_type(pycam.Toolpath.Bounds.TYPE_CUSTOM)
bounds_default_low = (model.minx, model.miny, model.minz)
bounds_default_high = (model.maxx, model.maxy, model.maxz)
# TODO: use the optparse conversion callback instead
def parse_triple_float(text):
nums = text.split(",")
if len(nums) != 3:
return None
result = []
for num in nums:
try:
result.append(float(num))
except ValueError:
return None
return result
bounds_lower_nums = parse_triple_float(opts.bounds_lower)
bounds_upper_nums = parse_triple_float(opts.bounds_upper)
if bounds_lower_nums is None \
and bounds_upper_nums is None:
# no bound was given
bounds.set_bounds(bounds_default_low, bounds_default_high)
elif bounds_lower_nums is None:
# only the upper bound was specified
bounds.set_bounds(bounds_default_low, bounds_upper_nums)
elif bounds_upper_nums is None:
# only the lower bound was specified
bounds.set_bounds(bounds_lower_nums, bounds_default_high)
else:
# both lower and upper bounds were specified
bounds.set_bounds(bounds_lower_nums, bounds_upper_nums)
# adjust the bounding box according to the "boundary_mode"
if opts.boundary_mode == "along":
offset = (0, 0, 0)
elif opts.boundary_mode == "inside":
offset = (-0.5 * opts.tool_diameter, -0.5 * opts.tool_diameter, 0)
else:
# "outside"
offset = (0.5 * opts.tool_diameter, 0.5 * opts.tool_diameter, 0)
process_bounds = Bounds(Bounds.TYPE_FIXED_MARGIN, offset, offset)
process_bounds.set_reference(bounds)
tps.set_bounds(process_bounds)
if opts.export_gcode:
# generate the toolpath
tp = pycam.Toolpath.Generator.generate_toolpath_from_settings(model, tps)
# write result
if isinstance(tp, basestring):
# an error occoured
log.error(tp)
else:
start_pos = tp.get_start_position()
meta_data = os.linesep.join(tp.get_meta_data())
if opts.export_gcode == "-":
destination = sys.stdout
close_destination = False
else:
destination = open(opts.export_gcode, "w")
close_destination = True
pycam.Exporters.SimpleGCodeExporter.ExportPathList(destination,
tp.get_path(), opts.unit_size, start_pos.x,
start_pos.y, start_pos.z, opts.tool_feedrate,
opts.tool_speed, safety_height=opts.safety_height,
max_skip_safety_distance=opts.tool_diameter,
comment=meta_data)
if close_destination:
destination.close()
if opts.export_task_config:
raise NotImplementedError("Export of task config is not " \
+ "implemented, yet!")