pycam 35.8 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
#!/usr/bin/env 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/>.
"""

25
# extend the PYTHONPATH to include the "src" directory
26 27
import sys
import os
sumpfralle's avatar
sumpfralle committed
28 29
BASE_DIR = os.path.dirname(os.path.realpath(__file__))
sys.path.insert(0, os.path.join(BASE_DIR, "src"))
30

31 32 33
# register the glut32.dll manually for the pyinstaller standalone executable
if hasattr(sys, "frozen") and sys.frozen and "_MEIPASS2" in os.environ:
    from ctypes import windll
sumpfralle's avatar
sumpfralle committed
34 35
    windll[os.path.join(os.path.normpath(os.environ["_MEIPASS2"]),
            "glut32.dll")]
36

37 38 39 40 41
import pycam.Gui.common as GuiCommon
import pycam.Gui.Settings
import pycam.Gui.Console
import pycam.Importers.TestModel
import pycam.Importers
42
import pycam.Exporters.GCodeExporter
43
import pycam.Toolpath.Generator
44
import pycam.Utils.threading
45
import pycam.Utils
46 47 48
from pycam.Toolpath import Bounds, Toolpath
from pycam import VERSION
import pycam.Utils.log
49
from optparse import OptionParser
50
import socket
51
import warnings
52 53
import logging
import time
54 55
# we need to import gtk.Warning to silence these warnings later
#import gtk
56

57 58 59 60
# we need the multiprocessing exception for remote connections
try:
    import multiprocessing
except ImportError:
61
    class multiprocessing(object):
62 63 64 65
        # use an arbitrary other Exception
        AuthenticationError = socket.error


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
# The installer for PyODE does not add the required PATH variable.
if pycam.Utils.get_platform() == pycam.Utils.PLATFORM_WINDOWS:
    os.environ["PATH"] = os.environ.get("PATH", "") + os.path.pathsep + sys.exec_prefix
# The GtkGLExt installer does not add the required PATH variable. 
if pycam.Utils.get_platform() == pycam.Utils.PLATFORM_WINDOWS:
    import _winreg
    path = None
    try:
        reg = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE)
        regkey = _winreg.OpenKey(reg, r"SOFTWARE\GtkGLExt\1.0\Runtime")
    except WindowsError:
        regkey = None
    index = 0
    while regkey:
        try:
            key, value = _winreg.EnumValue(regkey, index)[:2]
        except WindowsError:
            # no more items left
            break
        if key == "Path":
            path = os.path.join(str(value), "bin")
            break
        index += 1
    if path:
        os.environ["PATH"] = os.environ.get("PATH", "") + os.path.pathsep + path


93 94 95
log = pycam.Utils.log.get_logger()

EXAMPLE_MODEL_LOCATIONS = [
sumpfralle's avatar
sumpfralle committed
96
        os.path.join(BASE_DIR, "samples"),
97
        os.path.join(sys.prefix, "share", "pycam", "samples"),
sumpfralle's avatar
sumpfralle committed
98
        os.path.join(sys.prefix, "local", "share", "pycam", "samples"),
99 100 101
        os.path.join("usr", "share", "pycam", "samples")]
# for pyinstaller (windows distribution)
if "_MEIPASS2" in os.environ:
sumpfralle's avatar
sumpfralle committed
102 103
    EXAMPLE_MODEL_LOCATIONS.insert(0, os.path.join(os.path.normpath(
            os.environ["_MEIPASS2"]), "samples"))
sumpfralle's avatar
sumpfralle committed
104
DEFAULT_MODEL_FILE = "pycam-textbox.stl"
105
#DEFAULT_MODEL_FILE = "problem_1_triangle.stl"
106
EXIT_CODES = {"ok": 0, "requirements": 1, "load_model_failed": 2,
107 108
        "write_output_failed": 3, "parsing_failed": 4,
        "server_without_password": 5, "connection_error": 6}
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123


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("")
124 125
        full_report.append("Detailed list of requirements: %s" % \
                GuiCommon.REQUIREMENTS_LINK)
126
        log.critical(os.linesep.join(full_report))
127
        return EXIT_CODES["requirements"]
128 129 130 131

    gui = gui_class()

    # load the given model or the default
132
    if not inputfile:
133
        default_model = get_default_model()
134 135
        if isinstance(default_model, (basestring, pycam.Utils.URIHandler)):
            gui.load_model_file(filename=default_model, store_filename=False)
136 137 138 139 140 141 142 143 144
        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)

145 146 147
    # tell the GUI to empty the "undo" queue
    gui.finish_startup()

148 149
    # open the GUI
    gui.mainloop()
150 151
    # no error -> return no error code
    return None
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167


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()

168
def load_model_file(filename, program_locations, unit=None):
169 170 171 172 173
    uri = pycam.Utils.URIHandler(filename)
    if uri.is_local():
        uri = pycam.Utils.URIHandler(os.path.expanduser(str(filename)))
    if not uri.exists():
        log.warn("The input file ('%s') was not found!" % uri)
174
        return None
175 176
    importer = pycam.Importers.detect_file_type(uri)[1]
    model = importer(uri, program_locations=program_locations, unit=unit)
177
    if not model:
178
        log.warn("Failed to load the model file (%s)." % uri)
179 180 181 182 183 184 185 186 187
        return None
    else:
        return model

def get_output_handler(destination):
    if destination == "-":
        handler = sys.stdout
        closer = lambda: None
    else:
188 189
        # support paths with a tilde (~)
        destination = os.path.expanduser(destination)
190 191 192 193 194 195 196 197 198
        try:
            handler = open(destination, "w")
        except IOError, err_msg:
            log.error("Failed to open output file (%s) for writing: %s" \
                    % (destination, err_msg))
            return None, None
        closer = handler.close
    return (handler, closer)

199
def execute(parser, opts, args, pycam):
200
    # try to change the process name
201
    pycam.Utils.setproctitle("pycam")
202

203
    if len(args) > 0:
204
        inputfile = pycam.Utils.URIHandler(args[0])
205 206 207
    else:
        inputfile = None

208 209 210
    if opts.trace:
        log.setLevel(logging.DEBUG / 2)
    elif opts.debug:
211 212
        log.setLevel(logging.DEBUG)
    elif opts.quiet:
213 214 215
        log.setLevel(logging.WARNING)
        # disable the progress bar
        opts.progress = "none"
216 217 218 219 220 221 222 223 224
        # silence all warnings
        warnings.filterwarnings("ignore")
    else:
        # silence gtk warnings
        try:
            import gtk
            warnings.filterwarnings("ignore", category=gtk.Warning)
        except ImportError:
            pass
225 226 227 228 229 230 231

    # show version and exit
    if opts.show_version:
        if opts.quiet:
            # print only the bare version number
            print VERSION
        else:
232 233 234 235 236 237 238 239
            text = '''PyCAM %s
Copyright (C) 2008-2010 Lode Leroy
Copyright (C) 2010-2011 Lars Kruse

License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.''' % VERSION
            print text
240
        return EXIT_CODES["ok"]
241 242 243 244 245 246 247 248 249 250 251 252

    if not opts.disable_psyco:
        try:
            import psyco
            psyco.full()
            log.info("Psyco enabled")
        except ImportError:
            log.info("Psyco is not available (performance will probably " \
                    + "suffer slightly)")
    else:
        log.info("Psyco was disabled via the commandline")

253 254 255 256 257 258 259
    # check if server-auth-key is given -> this is mandatory for server mode
    if (opts.enable_server or opts.start_server) and not opts.server_authkey:
        parser.error("You need to supply a shared secret for server mode. " \
                + "This is supposed to prevent you from exposing your host " \
                + "to remote access without authentication.\n" \
                + "Please add the '--server-auth-key' argument followed by " \
                + "a shared secret password.")
260
        return EXIT_CODES["server_without_password"]
261

262
    # initialize multiprocessing
263 264 265 266 267 268
    try:
        if opts.start_server:
            pycam.Utils.threading.init_threading(opts.parallel_processes,
                    remote=opts.remote_server, run_server=True,
                    server_credentials=opts.server_authkey)
            pycam.Utils.threading.cleanup()
269
            return EXIT_CODES["ok"]
270 271 272 273 274 275
        else:
            pycam.Utils.threading.init_threading(opts.parallel_processes,
                    enable_server=opts.enable_server, remote=opts.remote_server,
                    server_credentials=opts.server_authkey)
    except socket.error, err_msg:
        log.error("Failed to connect to remote server: %s" % err_msg)
276
        return EXIT_CODES["connection_error"]
277 278 279
    except multiprocessing.AuthenticationError, err_msg:
        log.error("The remote server rejected your authentication key: %s" \
                % err_msg)
280
        return EXIT_CODES["connection_error"]
281

282 283 284 285 286 287 288 289 290 291 292 293 294
    # initialize the progress bar
    progress_styles = {"none": pycam.Gui.Console.ConsoleProgressBar.STYLE_NONE,
            "text": pycam.Gui.Console.ConsoleProgressBar.STYLE_TEXT,
            "bar": pycam.Gui.Console.ConsoleProgressBar.STYLE_BAR,
            "dot": pycam.Gui.Console.ConsoleProgressBar.STYLE_DOT,
    }
    progress_bar = pycam.Gui.Console.ConsoleProgressBar(sys.stdout,
            progress_styles[opts.progress])

    if opts.config_file:
        opts.config_file = os.path.expanduser(opts.config_file)

    if not opts.export_gcode and not opts.export_task_config:
295 296 297 298
        result = show_gui(inputfile, opts.config_file)
        if not result is None:
            # deliver the error code to our caller
            return result
299 300 301 302 303 304 305 306 307 308
    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_spindle_speed,
                opts.tool_feedrate)
309
        if opts.support_type == "grid":
310
            tps.set_support_grid(opts.support_grid_distance_x,
311 312 313 314 315 316 317 318 319 320 321 322 323
                    opts.support_grid_distance_y,
                    opts.support_profile_thickness, opts.support_profile_height,
                    opts.support_grid_offset_x, opts.support_grid_offset_y)
        elif opts.support_type in ("distributed-edges", "distributed-corners"):
            start_at_corners = (opts.support_type == "distributed-corners")
            tps.set_support_distributed(opts.support_distributed_distance,
                    opts.support_distributed_minimum,
                    opts.support_profile_thickness, opts.support_profile_height,
                    opts.support_distributed_length,
                    start_at_corners=start_at_corners)
        elif opts.support_type == "none":
            pass
        else:
324
            raise NotImplementedError, "Invalid support type specified: %s" % \
325
                    opts.support_type
326 327 328
        if opts.collision_engine == "ode":
            tps.set_calculation_backend("ODE")
        tps.set_unit_size(opts.unit_size)
329 330 331 332 333 334 335
        path_generator, postprocessor = {
                "layer": ("PushCutter", "SimpleCutter"),
                "contour-follow": ("ContourFollow", "SimpleCutter"),
                "contour-polygon": ("PushCutter", "ContourCutter"),
                "surface": ("DropCutter", "PathAccumulator"),
                "engrave": ("EngraveCutter", "SimpleCutter"),
            }[opts.process_path_strategy]
336
        tps.set_process_settings(path_generator, postprocessor,
337
                opts.process_path_direction,
338
                material_allowance=opts.process_material_allowance,
sumpfralle's avatar
sumpfralle committed
339
                overlap_percent=opts.process_overlap_percent,
340 341 342
                step_down=opts.process_step_down,
                engrave_offset=opts.process_engrave_offset,
                milling_style=opts.process_milling_style)
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359
        # set locations of external programs
        program_locations = {}
        if opts.external_program_inkscape:
            program_locations["inkscape"] = opts.external_program_inkscape
        if opts.external_program_pstoedit:
            program_locations["pstoedit"] = opts.external_program_pstoedit
        # load the model
        if inputfile is None:
            model = get_default_model()
            # the "get_default_model" function returns a string or a model
            if isinstance(model, basestring):
                model = load_model_file(model,
                        program_locations=program_locations,
                        unit=opts.unit_size)
        else:
            model = load_model_file(inputfile,
                    program_locations=program_locations, unit=opts.unit_size)
360
        if not model:
361
            # something went wrong - we quit
362
            return EXIT_CODES["load_model_failed"]
363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396
        # calculate the processing boundary
        bounds = Bounds()
        bounds.set_reference(model.get_bounds())
        # set the bounds type and let the default bounding box match the model
        if opts.bounds_type == "relative-margin":
            bounds.set_type(Bounds.TYPE_RELATIVE_MARGIN)
            bounds_default_low = (0, 0, 0)
            bounds_default_high = (0, 0, 0)
        elif opts.bounds_type == "fixed-margin":
            bounds.set_type(Bounds.TYPE_FIXED_MARGIN)
            bounds_default_low = (0, 0, 0)
            bounds_default_high = (0, 0, 0)
        else:
            # custom boundary setting
            bounds.set_type(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:
                    if num == "":
                        result.append(0.0)
                    else:
                        return None
            return result
        bounds_lower_nums = parse_triple_float(opts.bounds_lower)
        if opts.bounds_lower and not bounds_lower_nums:
397
            parser.error("Failed to parse the lower boundary limit: %s" \
398
                    % opts.bounds_lower)
399
            return EXIT_CODES["parsing_failed"]
400 401
        bounds_upper_nums = parse_triple_float(opts.bounds_upper)
        if opts.bounds_upper and not bounds_upper_nums:
402
            parser.error("Failed to parse the upper boundary limit: %s" \
403
                    % opts.bounds_upper)
404
            return EXIT_CODES["parsing_failed"]
405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438
        if bounds_lower_nums is None:
            bounds_lower_nums = bounds_default_low
        if bounds_upper_nums is None:
            bounds_upper_nums = bounds_default_high
        # 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
            start_time = time.time()
            toolpath = pycam.Toolpath.Generator.generate_toolpath_from_settings(
                    model, tps, callback=progress_bar.update)
            progress_bar.finish()
            log.info("Toolpath generation time: %f" \
                    % (time.time() - start_time))
            # write result
            if isinstance(toolpath, basestring):
                # an error occoured
                log.error(toolpath)
            else:
                description = "Toolpath generated via PyCAM v%s" % VERSION
                tp_obj = Toolpath(toolpath, description, tps)
                handler, closer = get_output_handler(opts.export_gcode)
                if handler is None:
439
                    return EXIT_CODES["write_output_failed"]
440 441
                generator = pycam.Exporters.GCodeExporter.GCodeGenerator(
                        handler, metric_units = (opts.unit_size == "mm"),
442
                        safety_height=opts.safety_height,
443
                        toggle_spindle_status=opts.gcode_no_start_stop_spindle,
444
                        minimum_steps=[opts.gcode_minimum_step])
445 446 447 448 449 450 451 452 453 454 455 456 457
                generator.set_speed(opts.tool_feedrate, opts.tool_spindle_speed)
                path_mode = opts.gcode_path_mode
                PATH_MODES = pycam.Exporters.GCodeExporter.PATH_MODES
                if (path_mode == "continuous") \
                        and (not opts.gcode_motion_tolerance is None):
                    if opts.gcode_naive_tolerance == 0:
                        naive_tolerance = None
                    else:
                        naive_tolerance = opts.gcode_naive_tolerance
                    generator.set_path_mode(PATH_MODES["continuous"],
                            opts.gcode_motion_tolerance, naive_tolerance)
                else:
                    generator.set_path_mode(PATH_MODES[opts.gcode_path_mode])
458
                generator.add_moves(tp_obj.get_moves(opts.safety_height),
459
                        comment=tp_obj.get_meta_data())
460
                generator.finish()
461 462 463 464
                closer()
        if opts.export_task_config:
            handler, closer = get_output_handler(opts.export_task_config)
            if handler is None:
465
                return EXIT_CODES["write_output_failed"]
466
            print >> handler, tps.get_string()
467
            closer()
468 469
    # no error -> don't return a specific exit code
    return None
470

471 472 473

# define the commandline interface
if __name__ == "__main__":
474 475 476 477 478 479
    # The PyInstaller standalone executable requires this "freeze_support" call.
    # Otherwise we will see a warning regarding an invalid argument called
    # "--multiprocessing-fork". This problem can be triggered on single-core
    # systems with these arguments: "--enable-server --server-auth-key foo".
    if hasattr(multiprocessing, "freeze_support"):
        multiprocessing.freeze_support()
480 481
    parser = OptionParser(prog="PyCAM",
            usage="usage: pycam [options] [inputfile]\n\n" \
482 483 484 485
                + "Start the PyCAM toolpath generator. Supplying one of " \
                + "the '--export-?' parameters will cause PyCAM to start " \
                + "in batch mode. Most parameters are useful only for " \
                + "batch mode.",
486
            epilog="PyCAM website: http://pycam.sf.net")
487 488 489 490 491 492
    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. " \
            + "These options trigger the non-interactive mode. Thus the GUI " \
            + "is disabled.")
    group_tool = parser.add_option_group("Tool definition",
493
            "Specify the tool parameters. The default tool is spherical and " \
494 495 496 497 498 499 500 501 502 503 504 505 506 507
            + "has a diameter of 1 unit. The default speeds are 1000 " \
            + "units/minute (feedrate) and 250 (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).")
508 509 510 511 512 513
    group_support_structure = parser.add_option_group("Support structure",
            "An optional support structure can be used to keep the object in " \
            + "place during the mill operation. The support structure can be " \
            + "removed manually afterwards. Various types of support " \
            + "structures are available. Support structures are disabled " \
            + "by default.")
514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539
    group_gcode = parser.add_option_group("GCode settings",
            "Specify some details of the generated GCode.")
    group_external_programs = parser.add_option_group("External programs",
            "Some optional external programs are used for format conversions.")
    # general options
    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="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_general.add_option("", "--disable-psyco", dest="disable_psyco",
            default=False, action="store_true", help="disable the Psyco " \
540
                    + "just-in-time-compiler even if it is available")
541 542 543
    group_general.add_option("", "--number-of-processes",
            dest="parallel_processes", default=None, type="int", action="store",
            help="override the default detection of multiple CPU cores. " \
544 545 546
                    + "Parallel processing only works with Python 2.6 (or " \
                    + "later) or with the additional 'multiprocessing' " \
                    + "module.")
547 548 549
    group_general.add_option("", "--enable-server", dest="enable_server",
            default=False, action="store_true", help="enable a local server " \
                    + "and (optionally) remote worker servers.")
550 551 552 553 554 555 556 557 558 559 560 561
    group_general.add_option("", "--remote-server", dest="remote_server",
            default=None, action="store", type="string", help="Connect to a " \
                    + "remote task server to distribute the processing load. " \
                    + "The server is given as an IP or a hostname with an " \
                    + "optional port (default: 1250) separated by a colon.")
    group_general.add_option("", "--start-server-only", dest="start_server",
            default=False, action="store_true", help="Start only a local " \
                    + "server for handling remote requests.")
    group_general.add_option("", "--server-auth-key", dest="server_authkey",
            default="", action="store", type="string", help="Secret used for " \
                    + "connecting to a remote server or for granting access " \
                    + "to remote clients.")
562
    group_general.add_option("-q", "--quiet", dest="quiet",
563 564
            default=False, action="store_true", help="output only warnings " \
            + "and errors.")
565 566 567
    group_general.add_option("-d", "--debug", dest="debug",
            default=False, action="store_true", help="enable output of debug " \
            + "messages.")
568 569 570
    group_general.add_option("", "--trace", dest="trace",
            default=False, action="store_true", help="enable more verbose " + \
            "debug messages.")
571 572 573 574 575 576 577 578 579
    group_general.add_option("", "--progress", dest="progress",
            default="text", action="store", type="choice",
            choices=["none", "text", "bar", "dot"],
            help="specify the type of progress bar used in non-GUI mode. " \
            + "The following options are available: text, none, bar, dot.")
    group_general.add_option("", "--profiling", dest="profile_destination",
            action="store", type="string",
            help="store profiling statistics in a file (only for debugging)")
    group_general.add_option("-v", "--version", dest="show_version",
580
            default=False, action="store_true", help="output the current " \
581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616
            + "version of PyCAM and exit")
    # export options
    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)")
    # tool options
    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-spindle-speed", dest="tool_spindle_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)")
    # process options
    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)")
617 618 619 620
    group_process.add_option("", "--process-path-strategy",
            dest="process_path_strategy", default="surface", action="store",
            type="choice", choices=["layer", "contour-follow",
                    "contour-polygon", "surface", "engrave"],
621
            help="one of the available toolpath strategies (layer, surface, " \
622
            + "contour-follow, contour-polygon, engrave)")
623 624 625 626 627 628 629 630 631 632 633 634
    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)")
635 636 637 638 639
    group_process.add_option("", "--process-milling-style",
            dest="process_milling_style", default="ignore",
            action="store", type="choice",
            choices=["ignore", "conventional", "climb"],
            help="milling style (conventional / climb / ignore)")
640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661
    group_process.add_option("", "--safety-height", dest="safety_height",
            default=25.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)")
    # bounds settings
    group_bounds.add_option("", "--bounds-type", dest="bounds_type",
            default="fixed-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')")
    # support grid settings
662 663 664 665 666 667 668 669 670 671 672 673
    group_support_structure.add_option("", "--support-type",
            dest="support_type", default="none", type="choice", action="store",
            choices=["none", "grid", "distributed-edges",
                    "distributed-corners"],
            help="type of the support structure (default: none)")
    group_support_structure.add_option("", "--support-profile-height",
            dest="support_profile_height", default=2.0, action="store",
            type="float", help="height of the support profile")
    group_support_structure.add_option("", "--support-profile-thickness",
            dest="support_profile_thickness", default=0.5, action="store",
            type="float", help="width of the support profile")
    group_support_structure.add_option("", "--support-grid-distance-x",
674 675
            dest="support_grid_distance_x", default=10.0, action="store",
            type="float", help="distance along the x-axis between two " \
676 677 678
            + "adjacent parallel lines of the support structure" \
            + "(only for grid type)")
    group_support_structure.add_option("", "--support-grid-distance-y",
679 680
            dest="support_grid_distance_y", default=10.0, action="store",
            type="float", help="distance along the y-axis between two " \
681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698
            + "adjacent parallel lines of the support structure " \
            + "(only for grid type)")
    group_support_structure.add_option("", "--support-grid-offset-x",
            dest="support_grid_offset_x", default=0.0, action="store",
            type="float", help="shift the support grid along the x axis")
    group_support_structure.add_option("", "--support-grid-offset-y",
            dest="support_grid_offset_y", default=0.0, action="store",
            type="float", help="shift the support grid along the y axis")
    group_support_structure.add_option("", "--support-distributed-distance",
            dest="support_distributed_distance", default=30.0, action="store",
            type="float", help="average distance between two adjacent " \
            + "support bridges")
    group_support_structure.add_option("", "--support-distributed-minimum",
            dest="support_distributed_minimum", default=2, action="store",
            type="int", help="minimum number of support bridges per polygon")
    group_support_structure.add_option("", "--support-distributed-length",
            dest="support_distributed_length", default=5.0, action="store",
            type="float", help="length of each support bridge")
699 700 701 702 703
    # gcode options
    group_gcode.add_option("", "--gcode-no-start-stop-spindle",
            dest="gcode_no_start_stop_spindle", default=True,
            action="store_false", help="do not start the spindle before " \
            + "and stop it after each operation (M3/M5)")
704
    group_gcode.add_option("", "--gcode-minimum-step",
705
            dest="gcode_minimum_step", default=0.00001,
706 707
            type="float", action="store", help="mimimum axial distance " \
            + "between two machine positions. Any shorter move is not " \
708
            + "written to GCode (default: 0.00001).")
709 710 711
    group_gcode.add_option("", "--gcode-path-mode", dest="gcode_path_mode",
            default="exact_path", action="store", type="choice",
            choices=["exact_path", "exact_stop", "continuous"],
712 713 714 715 716
            help="choose the GCode path mode from 'exact_path', 'exact_stop' " \
            + "and 'continuous'. Use '--gcode-motion-tolerance' and " \
            + "and '--gcode-naive-tolerance' if you want to limit the " \
            + "deviation. See http://linuxcnc.org/docs/html/gcode_main.html " \
            + "(G61) for details.")
717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735
    group_gcode.add_option("", "--gcode-motion-tolerance",
            dest="gcode_motion_tolerance", default=None,
            action="store", help="the optional motion tolerance for " \
            + "'continuous' path mode (G64).")
    group_gcode.add_option("", "--gcode-naive-tolerance",
            dest="gcode_naive_tolerance", default=None,
            action="store", help="the optional naive CAM tolerance for " \
            + "'continuous' path mode (G64).")
    # external program settings
    group_external_programs.add_option("", "--location-inkscape",
            dest="external_program_inkscape", default="", action="store",
            type="string", help="location of the Inkscape executable. " \
            + "This program is required for importing SVG files.")
    group_external_programs.add_option("", "--location-pstoedit",
            dest="external_program_pstoedit", default="", action="store",
            type="string", help="location of the PStoEdit executable. " \
            + "This program is required for importing SVG files.")
    (opts, args) = parser.parse_args()

736 737 738
    try:
        if opts.profile_destination:
            import cProfile
739
            exit_code = cProfile.run('execute(parser, opts, args, pycam)',
740
                    opts.profile_destination)
741 742 743
        else:
            # We need to add the parameter "pycam" to avoid weeeeird namespace
            # issues. Any idea how to fix this?
744
            exit_code = execute(parser, opts, args, pycam)
745 746
    except KeyboardInterrupt:
        log.info("Quit requested")
747
        exit_code = None
748
    pycam.Utils.threading.cleanup()
749 750 751 752
    if not exit_code is None:
        sys.exit(exit_code)
    else:
        sys.exit(EXIT_CODES["ok"])
753