#!/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/>. """ 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"), os.path.join(sys.prefix, "share", "pycam", "samples"), os.path.join("usr", "share", "pycam", "samples")) 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_general.add_option("", "--disable-psyco", dest="disable_psyco", default=False, action="store_true", help="disable the Psyco " \ + "just-in-time-compiler even when it is available") 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.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") 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!")