def update_model_dimensions(self, widget=None): dimension_bar = self.gui.get_object("DimensionTable") models = [m.get_model() for m in self.core.get("models").get_visible()] model_box = pycam.Geometry.Model.get_combined_bounds(models) if model_box is None: model_box = Box3D(Point3D(0, 0, 0), Point3D(0, 0, 0)) bounds = self.core.get("bounds").get_selected() if self.core.get("show_dimensions"): for value, label_suffix in ((model_box.lower.x, "XMin"), (model_box.upper.x, "XMax"), (model_box.lower.y, "YMin"), (model_box.upper.y, "YMax"), (model_box.lower.z, "ZMin"), (model_box.upper.z, "ZMax")): label_name = "ModelCorner%s" % label_suffix value = "%.3f" % value if label_suffix.lower().endswith("max"): value += self.core.get("unit_string") self.gui.get_object(label_name).set_label(value) if bounds: bounds_box = bounds.get_absolute_limits() if bounds_box is None: bounds_size = ("", "", "") else: bounds_size = [ "%.3f %s" % (high - low, self.core.get("unit_string")) for low, high in zip(bounds_box.lower, bounds_box.upper) ] for axis, size_string in zip("xyz", bounds_size): self.gui.get_object("model_dim_" + axis).set_text(size_string) dimension_bar.show() else: dimension_bar.hide()
def get_combined_bounds(models): low = [None, None, None] high = [None, None, None] for model in models: if (low[0] is None) or ((model.minx is not None) and (model.minx < low[0])): low[0] = model.minx if (low[1] is None) or ((model.miny is not None) and (model.miny < low[1])): low[1] = model.miny if (low[2] is None) or ((model.minz is not None) and (model.minz < low[2])): low[2] = model.minz if (high[0] is None) or ((model.maxx is not None) and (model.maxx > high[0])): high[0] = model.maxx if (high[1] is None) or ((model.maxy is not None) and (model.maxy > high[1])): high[1] = model.maxy if (high[2] is None) or ((model.maxz is not None) and (model.maxz > high[2])): high[2] = model.maxz if None in low or None in high: return None else: return Box3D(Point3D(*low), Point3D(*high))
def xtest_fixed_grid(self): box = Box3D(Point3D(-1, -1, -1), Point3D(1, 1, 1)) fixed_grid = get_fixed_grid(box, 0.5, line_distance=0.8, step_width=0.6, grid_direction=GridDirection.X, milling_style=MillingStyle.IGNORE, start_position=StartPosition.Z) resolved_fixed_grid = [list(layer) for layer in fixed_grid] print(resolved_fixed_grid)
def _get_bounds(self, models=None): if not models: models = self.core.get("models").get_selected() models = [m.model for m in models] box = pycam.Geometry.Model.get_combined_bounds(models) if box is None: return None else: # TODO: the x/y offset should be configurable via a control margin = 5 return Box3D(Point3D(box.lower.x - margin, box.lower.y - margin, box.lower.z), Point3D(box.upper.x + margin, box.upper.y + margin, box.upper.z))
def get_absolute_limits(self, tool_radius=None, models=None): get_low_value = lambda axis: self["parameters"]["BoundaryLow%s" % "XYZ" [axis]] get_high_value = lambda axis: self["parameters"]["BoundaryHigh%s" % "XYZ"[axis]] if self["parameters"]["TypeRelativeMargin"]: # choose the appropriate set of models if self["parameters"]["Models"]: # configured models always take precedence models = self["parameters"]["Models"] elif models: # use the supplied models (e.g. for toolpath calculation) pass else: # use all visible models -> for live visualization models = self.core.get("models").get_visible() model_box = pycam.Geometry.Model.get_combined_bounds( [model.model for model in models]) if model_box is None: # zero-sized models -> no action return None is_percent = _RELATIVE_UNIT[self["parameters"] ["RelativeUnit"]] == "%" low, high = [], [] if is_percent: for axis in range(3): dim = model_box.upper[axis] - model_box.lower[axis] low.append(model_box.lower[axis] - (get_low_value(axis) / 100.0 * dim)) high.append(model_box.upper[axis] + (get_high_value(axis) / 100.0 * dim)) else: for axis in range(3): low.append(model_box.lower[axis] - get_low_value(axis)) high.append(model_box.upper[axis] + get_high_value(axis)) else: low, high = [], [] for axis in range(3): low.append(get_low_value(axis)) high.append(get_high_value(axis)) tool_limit = _BOUNDARY_MODES[self["parameters"]["ToolLimit"]] # apply inside/along/outside if a tool is given if tool_radius and (tool_limit != "along"): if tool_limit == "inside": offset = -tool_radius else: offset = tool_radius # apply offset only for x and y for index in range(2): low[index] -= offset high[index] += offset return Box3D(Point3D(*low), Point3D(*high))
def get_lines_grid(models, box, layer_distance, line_distance=None, step_width=None, milling_style=MillingStyle.CONVENTIONAL, start_position=StartPosition.Z, pocketing_type=PocketingType.NONE, skip_first_layer=False, callback=None): _log.debug("Calculating lines grid: {} model(s), z={}..{} ({}), line_distance={}, " "step_width={}".format(len(models), box.lower, box.upper, layer_distance, line_distance, step_width)) # the lower limit is never below the model polygons = _get_sorted_polygons(models, callback=callback) if polygons: low_limit_lines = min([polygon.minz for polygon in polygons]) new_lower = Point3D(box.lower.x, box.lower.y, max(box.lower.z, low_limit_lines)) box = Box3D(new_lower, box.upper) # calculate pockets if pocketing_type != PocketingType.NONE: if callback is not None: callback(text="Generating pocketing polygons ...") polygons = get_pocketing_polygons(polygons, line_distance, pocketing_type, callback=callback) # extract lines in correct order from all polygons lines = [] for polygon in polygons: if callback: callback() if polygon.is_closed and (milling_style == MillingStyle.CONVENTIONAL): polygon = polygon.copy() polygon.reverse_direction() for line in polygon.get_lines(): lines.append(line) if isiterable(layer_distance): layers = layer_distance elif layer_distance is None: # only one layer layers = [box.lower.z] else: layers = floatrange(box.lower.z, box.upper.z, inc=layer_distance, reverse=bool(start_position & StartPosition.Z)) # turn the generator into a list - otherwise the slicing fails layers = list(layers) # engrave ignores the top layer if skip_first_layer and (len(layers) > 1): layers = layers[1:] last_z = None _log.debug("Pocketing Polygon Layers: %d", len(layers)) if layers: # the upper layers are used for PushCutter operations for z in layers[:-1]: _log.debug2("Pocketing Polygon Layers: calculating z=%g for PushCutter", z) if callback: callback() yield get_lines_layer(lines, z, last_z=last_z, step_width=None, milling_style=milling_style) last_z = z # the last layer is used for a DropCutter operation if callback: callback() _log.debug2("Pocketing Polygon Layers: calculating z=%g (lowest layer) for DropCutter", layers[-1]) yield get_lines_layer(lines, layers[-1], last_z=last_z, step_width=step_width, milling_style=milling_style)
def get_absolute_limits(self, reference=None): """ calculate the current absolute limits of the Bounds instance @value reference: a reference object described by a tuple (or list) of three item. These three values describe only the lower boundary of this object (for the x, y and z axes). Each item must be a float value. This argument is ignored for the boundary type "TYPE_CUSTOM". @type reference: (tuple|list) of float @returns: a tuple of two lists containg the low and high limits @rvalue: tuple(list) """ # use the default reference if none was given if reference is None: reference = self.reference # check if a reference is given (if necessary) if self.bounds_type \ in (Bounds.TYPE_RELATIVE_MARGIN, Bounds.TYPE_FIXED_MARGIN): if reference is None: raise ValueError( "any non-custom boundary definition requires a reference " "object for calculating absolute limits") else: ref_low, ref_high = reference.get_absolute_limits() low = [None] * 3 high = [None] * 3 # calculate the absolute limits if self.bounds_type == Bounds.TYPE_RELATIVE_MARGIN: for index in range(3): dim_width = ref_high[index] - ref_low[index] low[index] = ref_low[index] - self.bounds_low[index] * dim_width high[index] = ref_high[ index] + self.bounds_high[index] * dim_width elif self.bounds_type == Bounds.TYPE_FIXED_MARGIN: for index in range(3): low[index] = ref_low[index] - self.bounds_low[index] high[index] = ref_high[index] + self.bounds_high[index] elif self.bounds_type == Bounds.TYPE_CUSTOM: for index in range(3): low[index] = number(self.bounds_low[index]) high[index] = number(self.bounds_high[index]) else: # this should not happen raise NotImplementedError( "the function 'get_absolute_limits' is currently not " "implemented for the bounds_type '%s'" % str(self.bounds_type)) return Box3D(Point3D(low), Point3D(high))
def run_dropcutter(): """ Run DropCutter on standard PyCAM sample plaque """ progress_bar = ConsoleProgressBar(sys.stdout) overlap = .6 layer_distance = 1 tool = CylindricalCutter(10) path_generator = DropCutter(PathAccumulator()) bounds = Bounds(Bounds.TYPE_CUSTOM, Box3D(Point3D(model.minx-5, model.miny-5, model.minz), Point3D(model.maxx+5, model.maxy+5, model.maxz))) low, high = bounds.get_absolute_limits() line_distance = 2 * tool.radius * (1.0 - overlap) motion_grid = get_fixed_grid((low, high), layer_distance, line_distance, tool.radius / 4.0) path_generator.GenerateToolPath(tool, [model], motion_grid, minz=low[2], maxz=high[2], draw_callback=progress_bar.update)
def _get_bounds_instance_from_dict(self, indict): """ get Bounds instances for each bounds definition @value model: the model that should be used for relative margins @type model: pycam.Geometry.Model.Model or callable @returns: list of Bounds instances @rtype: list(Bounds) """ low_bounds = (indict["x_low"], indict["y_low"], indict["z_low"]) high_bounds = (indict["x_high"], indict["y_high"], indict["z_high"]) if indict["type"] == "relative_margin": bounds_type = Bounds.TYPE_RELATIVE_MARGIN elif indict["type"] == "fixed_margin": bounds_type = Bounds.TYPE_FIXED_MARGIN else: bounds_type = Bounds.TYPE_CUSTOM new_bound = Bounds(bounds_type, Box3D(Point3D(*low_bounds), Point3D(*high_bounds))) new_bound.set_name(indict["name"]) return new_bound
def test_fixed_grid(self): box = Box3D(Point3D(-3, -2, -1), Point3D(3, 2, 1)) grid = _resolve_nested( 3, get_fixed_grid(box, 1.2, line_distance=2.0, step_width=None, grid_direction=GridDirection.X, milling_style=MillingStyle.CONVENTIONAL, start_position=StartPosition.Z)) self.assert_almost_equal_grid(grid, ( (((-3, -2, 1), (3, -2, 1)), ((-3, 0, 1), (3, 0, 1)), ((-3, 2, 1), (3, 2, 1))), (((3, 2, 0), (-3, 2, 0)), ((3, 0, 0), (-3, 0, 0)), ((3, -2, 0), (-3, -2, 0))), (((-3, -2, -1), (3, -2, -1)), ((-3, 0, -1), (3, 0, -1)), ((-3, 2, -1), (3, 2, -1))), ))
def __init__(self, bounds_type=None, box=None, reference=None): """ create a new Bounds instance @value bounds_type: any of TYPE_RELATIVE_MARGIN | TYPE_FIXED_MARGIN | TYPE_CUSTOM @type bounds_type: int @value bounds_low: the lower margin of the boundary compared to the reference object (for TYPE_RELATIVE_MARGIN | TYPE_FIXED_MARGIN) or the specific boundary values (for TYPE_CUSTOM). Only the lower values of the three axes (x, y and z) are given. @type bounds_low: (tuple|list) of float @value bounds_high: see 'bounds_low' @type bounds_high: (tuple|list) of float @value reference: optional default reference Bounds instance @type reference: Bounds """ self.name = "No name" self.set_type(bounds_type) if box is None: box = Box3D(Point3D(0, 0, 0), Point3D(0, 0, 0)) self.set_bounds(box) self.reference = reference
def _shift_to_origin(self, position, callback=None): if position != Point3D(0, 0, 0): self.shift(*(pmul(position, -1)), callback=callback)
def change_unit_apply(self, widget=None, data=None, apply_scale=True): # TODO: move tool/process/task related code to these plugins new_unit = self.gui.get_object("unit_control").get_active_text() factors = {("mm", "inch"): 1 / 25.4, ("inch", "mm"): 25.4} conversion = (self._last_unit, new_unit) if conversion in factors.keys(): factor = factors[conversion] if apply_scale: if self.gui.get_object("UnitChangeModel").get_active(): # transform the model if it is selected # keep the original center of the model self.core.emit_event("model-change-before") models = self.core.get("models") progress = self.core.get("progress") progress.disable_cancel() progress.set_multiple(len(models), "Scaling model") for model in models: new_x, new_y, new_z = ((model.maxx + model.minx) / 2, (model.maxy + model.miny) / 2, (model.maxz + model.minz) / 2) model.scale(factor, callback=progress.update) cur_x, cur_y, cur_z = self._get_model_center() model.shift(new_x - cur_x, new_y - cur_y, new_z - cur_z, callback=progress.update) progress.update_multiple() progress.finish() if self.gui.get_object("UnitChangeProcesses").get_active(): # scale the process settings for process in self.core.get("processes"): for key in ("MaterialAllowanceControl", "MaxStepDownControl", "EngraveOffsetControl"): process[key] *= factor if self.gui.get_object("UnitChangeBounds").get_active(): # scale the boundaries and keep their center for bounds in self.core.get("bounds"): box = bounds.get_bounds() if bounds.get_type() == Bounds.TYPE_FIXED_MARGIN: low = Point3D(pmul(box.lower, factor)) high = Point3D(pmul(box.upper, factor)) bounds.set_bounds(Box3D(low, high)) elif bounds.get_type() == Bounds.TYPE_CUSTOM: center = box.get_center() low = Point3D(*[ c - factor * (c - l) for l, c in zip(box.lower, center) ]) high = Point3D(*[ c + factor * (h - c) for h, c in zip(box.upper, center) ]) bounds.set_bounds(Box3D(low, high)) elif bounds.get_type() == Bounds.TYPE_RELATIVE_MARGIN: # no need to change relative margins pass if self.gui.get_object("UnitChangeTools").get_active(): # scale all tool dimensions for tool in self.core.get("tools"): for key in ("tool_radius", "torus_radius"): # TODO: fix this invalid access tool[key] *= factor self.unit_change_window.hide() # store the current unit (for the next run of this function) self._last_unit = new_unit # update all labels containing the unit size self.update_unit_labels() # redraw the model self.core.emit_event("model-change-after")
def get_bounds(self): low = (self.bounds["minx"], self.bounds["miny"], self.bounds["minz"]) high = (self.bounds["maxx"], self.bounds["maxy"], self.bounds["maxz"]) return Bounds(Bounds.TYPE_CUSTOM, Box3D(Point3D(low), Point3D(high)))
def get_bounds(self): return Box3D(Point3D(*self.bounds_low), Point3D(*self.bounds_high))
def _get_absolute_position(minx, maxx, miny, maxy, z, position): """ calculate a point within a rectangle based on the relative position along the axes """ x = maxx if position & StartPosition.X > 0 else minx y = maxy if position & StartPosition.Y > 0 else miny return Point3D(x, y, z)
def _shift_origin_to(self, position, callback=None): if position != Point3D(0, 0, 0): self.shift(*position, callback=callback)
def get_bounds(self): return Bounds( Bounds.TYPE_CUSTOM, Box3D(Point3D(self.minx, self.miny, self.minz), Point3D(self.maxx, self.maxy, self.maxz)))
def get_referenced_bounds(self, reference): return Bounds(self.bounds_type, Box3D(Point3D(*self.bounds_low), Point3D(*self.bounds_high)), reference=reference)