#!/usr/bin/python # -*- coding: utf-8 -*- """ $Id$ Copyright 2010 Lars Kruse 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 . """ 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.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() gui.load_model(get_model(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_model(inputfile=None): # try to load the default model file ("pycam" logo) if inputfile is None: for inputdir in EXAMPLE_MODEL_LOCATIONS: inputfile = os.path.join(inputdir, DEFAULT_MODEL_FILE) if os.path.isfile(inputfile): model = pycam.Importers.STLImporter.ImportModel(inputfile) break 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))) model = pycam.Importers.TestModel() else: model = pycam.Importers.STLImporter.ImportModel(inputfile) return model # check if we were started as a separate program if __name__ == "__main__": parser = OptionParser( usage="usage: %prog [options] [inputfile]", version="PyCAM v%s" % VERSION) 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("", "--disable-ode", dest="ode_disabled", default=False, action="store_true", help="disable experimental ODE collision detection") 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-radius", dest="tool_radius", default=1.0, action="store", type="float", help="radius of the tool") group_tool.add_option("", "--tool-torus-radius", dest="tool_torus_radius", default=0.25, action="store", type="float", help="torus radius 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. '12,5.5,0')") 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. '4,4,-0.5')") 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="how far apart should two adjacent parallel " \ + "lines of the support grid pattern be") 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 opts.ode_disabled: override_ode_availability(False) 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, opts.tool_radius, opts.tool_torus_radius, 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 not opts.ode_disabled: 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) # TODO: inside/along/outside boundary (currently: along) tps.set_bounds(bounds) tp = pycam.Toolpath.Generator.generate_toolpath_from_settings(model, tps) # write result print tp