def get_rotation_matrix_axis_angle(rot_axis, rot_angle, use_radians=True): """ calculate rotation matrix for a normalized vector and an angle see http://mathworld.wolfram.com/RotationMatrix.html @type rot_axis: tuple(float) @value rot_axis: the vector describes the rotation axis. Its length should be 1.0 (normalized). @type rot_angle: float @value rot_angle: rotation angle (radiant) @rtype: tuple(tuple(float)) @return: the rotation matrix (3x3) """ if not use_radians: rot_angle *= math.pi / 180 sin = number(math.sin(rot_angle)) cos = number(math.cos(rot_angle)) return ((cos + rot_axis[0] * rot_axis[0] * (1 - cos), rot_axis[0] * rot_axis[1] * (1 - cos) - rot_axis[2] * sin, rot_axis[0] * rot_axis[2] * (1 - cos) + rot_axis[1] * sin), (rot_axis[1] * rot_axis[0] * (1 - cos) + rot_axis[2] * sin, cos + rot_axis[1] * rot_axis[1] * (1 - cos), rot_axis[1] * rot_axis[2] * (1 - cos) - rot_axis[0] * sin), (rot_axis[2] * rot_axis[0] * (1 - cos) - rot_axis[1] * sin, rot_axis[2] * rot_axis[1] * (1 - cos) + rot_axis[0] * sin, cos + rot_axis[2] * rot_axis[2] * (1 - cos)))
def move_camera_by_screen(self, x_move, y_move, max_model_shift): """ move the camera acoording to a mouse movement @type x_move: int @value x_move: movement of the mouse along the x axis @type y_move: int @value y_move: movement of the mouse along the y axis @type max_model_shift: float @value max_model_shift: maximum shifting of the model view (e.g. for x_move == screen width) """ factors_x, factors_y = self._get_axes_vectors() width, height = self._get_screen_dimensions() # relation of x/y movement to the respective screen dimension win_x_rel = (-2 * x_move) / float(width) / math.sin(self.view["fovy"]) win_y_rel = (-2 * y_move) / float(height) / math.sin(self.view["fovy"]) # This code is completely arbitrarily based on trial-and-error for # finding a nice movement speed for all distances. # Anyone with a better approach should just fix this. distance_vector = self.get("distance") distance = float(sqrt(sum([dim**2 for dim in distance_vector]))) win_x_rel *= math.cos(win_x_rel / distance)**20 win_y_rel *= math.cos(win_y_rel / distance)**20 # update the model position that should be centered on the screen old_center = self.view["center"] new_center = [] for i in range(3): new_center.append(old_center[i] + max_model_shift * (number(win_x_rel) * factors_x[i] + number(win_y_rel) * factors_y[i])) self.view["center"] = tuple(new_center)
def get_support_grid_locations(minx, maxx, miny, maxy, dist_x, dist_y, offset_x=0.0, offset_y=0.0, adjustments_x=None, adjustments_y=None): """ calculate positions of a orthogonal grid of support bridges @param minx: minimum x value of the target area @param maxx: maximum x value of the target area @param miny: minimum y value of the target area @param maxy: maximum y value of the target area @param dist_x: distance between two lines parallel to the y axis @param dist_y: distance between two lines parallel to the x axis @param adjustments_x: iterable of offsets to be added to each x position @param adjustments_y: iterable of offsets to be added to each y position """ def get_lines(center, dist, min_value, max_value): """ generate a list of positions starting from the middle going up and and down """ if dist > 0: lines = [center] current = center while current - dist > min_value: current -= dist lines.insert(0, current) current = center while current + dist < max_value: current += dist lines.append(current) else: lines = [] # remove lines that are out of range (e.g. due to a huge offset) lines = [line for line in lines if min_value < line < max_value] return lines # convert all inputs to the type defined in "number" dist_x = number(dist_x) dist_y = number(dist_y) offset_x = number(offset_x) offset_y = number(offset_y) center_x = (maxx + minx) / 2 + offset_x center_y = (maxy + miny) / 2 + offset_y lines_x = get_lines(center_x, dist_x, minx, maxx) lines_y = get_lines(center_y, dist_y, miny, maxy) # apply as many offsets from adjustments_x and adjustments_y as available for value_list, adjustments in ((lines_x, adjustments_x), (lines_y, adjustments_y)): if adjustments: for index, (current_value, adjustment) in enumerate(zip(value_list, adjustments)): value_list[index] = current_value + number(adjustment) return lines_x, lines_y
def _get_axes_vectors(self): """calculate the model vectors along the screen's x and y axes""" # The "up" vector defines, in what proportion each axis of the model is # in line with the screen's y axis. v_up = self.view["up"] factors_y = (number(v_up[0]), number(v_up[1]), number(v_up[2])) # Calculate the proportion of each model axis according to the x axis of # the screen. distv = self.view["distance"] distv = pnormalized((distv[0], distv[1], distv[2])) factors_x = pnormalized(pcross(distv, (v_up[0], v_up[1], v_up[2]))) return (factors_x, factors_y)
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 __init__(self, radius, location=None, height=None): super().__init__() if location is None: location = (0, 0, 0) if height is None: height = 10 radius = number(radius) self.height = number(height) self.radius = radius self.radiussq = radius**2 self.required_distance = 0 self.distance_radius = self.radius self.distance_radiussq = self.distance_radius**2 self.shape = {} self.location = location self.moveto(self.location) self.uuid = None self.update_uuid()
def get_offset_polygons_incremental(self, offset, depth=20): if offset == 0: return [self] if offset in self._cached_offset_polygons: return self._cached_offset_polygons[offset] def is_better_offset(previous_offset, alternative_offset): return (((offset < alternative_offset < 0) or (0 < alternative_offset < offset)) and (abs(alternative_offset) > abs(previous_offset))) # check the cache for a good starting point best_offset = 0 best_offset_polygons = [self] for cached_offset in self._cached_offset_polygons: if is_better_offset(best_offset, cached_offset): best_offset = cached_offset best_offset_polygons = self._cached_offset_polygons[ cached_offset] remaining_offset = offset - best_offset result_polygons = [] for poly in best_offset_polygons: result = poly.get_offset_polygons_validated(remaining_offset) if result is not None: result_polygons.extend(result) else: lower = number(0) upper = remaining_offset loop_limit = 90 while (loop_limit > 0): middle = (upper + lower) / 2 result = poly.get_offset_polygons_validated(middle) if result is None: upper = middle else: if depth > 0: # the original polygon was split or modified print("Next level: %s" % str(middle)) shifted_sub_polygons = [] for sub_poly in result: shifted_sub_polygons.extend( sub_poly.get_offset_polygons( remaining_offset - middle, depth=depth - 1)) result_polygons.extend(shifted_sub_polygons) break else: print("Maximum recursion level reached") break loop_limit -= 1 else: # no split event happened -> no valid shifted polygon pass self._cached_offset_polygons[offset] = result_polygons return result_polygons
def multiply_vector_matrix(v, m): """ Multiply a 3d vector with a 3x3 matrix. The result is a 3d vector. @type v: tuple(float) | list(float) @value v: a 3d vector as tuple or list containing three floats @type m: tuple(tuple(float)) | list(list(float)) @value m: a 3x3 list/tuple of floats @rtype: tuple(float) @return: a tuple of 3 floats as the matrix product """ if len(m) == 9: m = [number(value) for value in m] m = ((m[0], m[1], m[2]), (m[3], m[4], m[5]), (m[6], m[7], m[8])) else: new_m = [] for column in m: new_m.append([number(value) for value in column]) v = [number(value) for value in v] return (v[0] * m[0][0] + v[1] * m[0][1] + v[2] * m[0][2], v[0] * m[1][0] + v[1] * m[1][1] + v[2] * m[1][2], v[0] * m[2][0] + v[1] * m[2][1] + v[2] * m[2][2])
def get_support_grid(minx, maxx, miny, maxy, z_plane, dist_x, dist_y, thickness, height, length, offset_x=0.0, offset_y=0.0, adjustments_x=None, adjustments_y=None): lines_x, lines_y = get_support_grid_locations(minx, maxx, miny, maxy, dist_x, dist_y, offset_x, offset_y, adjustments_x, adjustments_y) # create all x grid lines grid_model = Model() # convert all inputs to "number" thickness = number(thickness) height = number(height) # helper variables thick_half = thickness / 2 for line_x in lines_x: # we make the grid slightly longer (by thickness) than necessary grid_model += _add_aligned_cuboid_to_model(line_x - thick_half, line_x + thick_half, miny - length, maxy + length, z_plane, z_plane + height) for line_y in lines_y: # we make the grid slightly longer (by thickness) than necessary grid_model += _add_aligned_cuboid_to_model(minx - length, maxx + length, line_y - thick_half, line_y + thick_half, z_plane, z_plane + height) return grid_model
def __init__(self, radius, minorradius, **kwargs): minorradius = number(minorradius) self.minorradius = minorradius # we need "minorradius" for "moveto" - thus set it before parent's init BaseCutter.__init__(self, radius, **kwargs) self.majorradius = self.radius - minorradius self.axis = (0, 0, 1) self.majorradiussq = self.majorradius**2 self.minorradiussq = self.minorradius**2 self.distance_majorradius = self.majorradius + self.get_required_distance( ) self.distance_minorradius = self.minorradius + self.get_required_distance( ) self.distance_majorradiussq = self.distance_majorradius**2 self.distance_minorradiussq = self.distance_minorradius**2
def auto_adjust_distance(self): v = self.view # adjust the distance to get a view of the whole object low_high = list(zip(*self._get_low_high_dims())) if (None, None) in low_high: return max_dim = max([high - low for low, high in low_high]) distv = pnormalized((v["distance"][0], v["distance"][1], v["distance"][2])) # The multiplier "1.25" is based on experiments. 1.414 (sqrt(2)) should # be roughly sufficient for showing the diagonal of any model. distv = pmul(distv, (max_dim * 1.25) / number(math.sin(v["fovy"] / 2))) self.view["distance"] = distv # Adjust the "far" distance for the camera to make sure, that huge # models (e.g. x=1000) are still visible. self.view["zfar"] = 100 * max_dim
def get_support_grid_locations(minx, maxx, miny, maxy, dist_x, dist_y, offset_x=0.0, offset_y=0.0, adjustments_x=None, adjustments_y=None): def get_lines(center, dist, min_value, max_value): """ generate a list of positions starting from the middle going up and and down """ if dist > 0: lines = [center] current = center while current - dist > min_value: current -= dist lines.insert(0, current) current = center while current + dist < max_value: current += dist lines.append(current) else: lines = [] # remove lines that are out of range (e.g. due to a huge offset) lines = [line for line in lines if min_value < line < max_value] return lines # convert all inputs to the type defined in "number" dist_x = number(dist_x) dist_y = number(dist_y) offset_x = number(offset_x) offset_y = number(offset_y) center_x = (maxx + minx) / 2 + offset_x center_y = (maxy + miny) / 2 + offset_y lines_x = get_lines(center_x, dist_x, minx, maxx) lines_y = get_lines(center_y, dist_y, miny, maxy) if adjustments_x: for index in range(min(len(lines_x), len(adjustments_x))): lines_x[index] += number(adjustments_x[index]) if adjustments_y: for index in range(min(len(lines_y), len(adjustments_y))): lines_y[index] += number(adjustments_y[index]) return lines_x, lines_y
def pdiv(a, c): c = number(c) return (a[0] / c, a[1] / c, a[2] / c)
def scale_distance(self, scale): if scale != 0: scale = number(scale) dist = self.view["distance"] self.view["distance"] = (scale * dist[0], scale * dist[1], scale * dist[2])
def pmul(a, c): c = number(c) return (a[0] * c, a[1] * c, a[2] * c)
def pmul(a, c): c = number(c) if not a: return (False, False, False) return (a[0] * c, a[1] * c, a[2] * c)
def set_required_distance(self, value): if value >= 0: self.required_distance = number(value) self.distance_radius = self.radius + self.get_required_distance() self.distance_radiussq = self.distance_radius * self.distance_radius self.update_uuid()
def get_offset_polygons(self, offset, callback=None): def simplify_polygon_intersections(lines): new_group = lines[:] # remove all non-adjacent intersecting lines (this splits the group) if len(new_group) > 0: group_starts = [] index1 = 0 while index1 < len(new_group): index2 = 0 while index2 < len(new_group): index_distance = min( abs(index2 - index1), abs(len(new_group) - (index2 - index1))) # skip neighbours if index_distance > 1: line1 = new_group[index1] line2 = new_group[index2] intersection, factor = line1.get_intersection( line2) if intersection and (pdist(intersection, line1.p1) > epsilon) \ and (pdist(intersection, line1.p2) > epsilon): del new_group[index1] new_group.insert(index1, Line(line1.p1, intersection)) new_group.insert(index1 + 1, Line(intersection, line1.p2)) # Shift all items in "group_starts" by one if # they reference a line whose index changed. for i in range(len(group_starts)): if group_starts[i] > index1: group_starts[i] += 1 if index1 + 1 not in group_starts: group_starts.append(index1 + 1) # don't update index2 -> maybe there are other hits elif intersection and (pdist( intersection, line1.p1) < epsilon): if index1 not in group_starts: group_starts.append(index1) index2 += 1 else: index2 += 1 else: index2 += 1 index1 += 1 # The lines intersect each other # We need to split the group. if len(group_starts) > 0: group_starts.sort() groups = [] last_start = 0 for group_start in group_starts: transfer_group = new_group[last_start:group_start] # add only non-empty groups if transfer_group: groups.append(transfer_group) last_start = group_start # Add the remaining lines to the first group or as a new # group. if groups[0][0].p1 == new_group[-1].p2: groups[0] = new_group[last_start:] + groups[0] else: groups.append(new_group[last_start:]) # try to find open groups that can be combined combined_groups = [] for index, current_group in enumerate(groups): # Check if the group is not closed: try to add it to # other non-closed groups. if current_group[0].p1 == current_group[-1].p2: # a closed group combined_groups.append(current_group) else: # the current group is open for other_group in groups[index + 1:]: if other_group[0].p1 != other_group[-1].p2: # This group is also open - a candidate # for merging? if other_group[0].p1 == current_group[ -1].p2: current_group.reverse() for line in current_group: other_group.insert(0, line) break if other_group[-1].p2 == current_group[ 0].p1: other_group.extend(current_group) break else: # not suitable open group found combined_groups.append(current_group) return combined_groups else: # just return one group without intersections return [new_group] else: return None offset = number(offset) if offset == 0: return [self] if self.is_outer(): inside_shifting = max(0, -offset) else: inside_shifting = max(0, offset) if inside_shifting * 2 >= self.get_max_inside_distance(): # This offset will not create a valid offset polygon. # Sadly there is currently no other way to detect a complete flip of # something like a circle. log.debug("Skipping offset polygon: polygon is too small") return [] points = [] for index in range(len(self._points)): points.append(self.get_shifted_vertex(index, offset)) new_lines = [] for index in range(len(points) - 1): p1 = points[index] p2 = points[(index + 1)] new_lines.append(Line(p1, p2)) if self.is_closed and (len(points) > 1): new_lines.append(Line(points[-1], points[0])) if callback and callback(): return None cleaned_line_groups = simplify_polygon_intersections(new_lines) if cleaned_line_groups is None: log.debug("Skipping offset polygon: intersections could not be " "simplified") return None else: if not cleaned_line_groups: log.debug("Skipping offset polygon: no polygons left after " "intersection simplification") groups = [] for lines in cleaned_line_groups: if callback and callback(): return None group = Polygon(self.plane) for line in lines: group.append(line) groups.append(group) if not groups: log.debug("Skipping offset polygon: toggled polygon removed") # remove all polygons that are within other polygons result = [] for group in groups: inside = False for group_test in groups: if callback and callback(): return None if group_test is group: continue if group_test.is_polygon_inside(group): inside = True if not inside: result.append(group) if not result: log.debug( "Skipping offset polygon: polygon is inside of another one" ) return result