def build_graph(milled_pocket, milling_diameter, fast_algorithm=False): """ return graph which will be used to compute milling path. you can choose between fast (linear) algorithm with good enough quality or get the optimal solution (in n^3) using "fast_algorithm" parameter. """ if __debug__: if is_module_debugged(__name__): print("creating graph out of pocket") tycat(milled_pocket) # fill all vertices graph = Graph() _create_vertices(milled_pocket, milling_diameter, graph) # finish by adding horizontal internal edges create_internal_edges(graph, milling_diameter) if __debug__: if is_module_debugged(__name__): print("created internal edges") tycat(graph) # prepare for eulerian path if fast_algorithm: make_degrees_even_fast(graph, milling_diameter) else: make_degrees_even(graph) if __debug__: if is_module_debugged(__name__): print("degrees made even") tycat(graph) return graph
def slice_stl_file(stl_file, slice_size, milling_radius): """ load stl file. cut into slices of wanted size and build polygons. return polygons arrays indexed by slice height. """ model = Stl(stl_file) margin = 2 * milling_radius + 0.01 border = border_2d(model, margin) if __debug__: if is_module_debugged(__name__): print("model loaded") print("slices are:") slices = model.compute_slices(slice_size, model.translation_vector(margin)) slices_polygons = {} # polygons in each slice, indexed by height for height in sorted(slices): stl_slice = slices[height] stl_slice.extend(border) if __debug__: if is_module_debugged(__name__): tycat(stl_slice) simpler_slice = merge_segments(stl_slice) slice_polygons = build_polygons(simpler_slice) slices_polygons[height] = slice_polygons return slices_polygons
def build(cls, milling_radius, polygons): """ figures out which polygon is included in which other. returns tree of all holed polygons to mill such that each node contains a holed polygon (except root) and each node cannot be milled before any of its ancestors. """ inclusion_tree = build_inclusion_tree(polygons) inclusion_tree.ascend_polygons() poly_tree = cls() _convert_inclusion_tree(poly_tree, inclusion_tree) if __debug__: if is_module_debugged(__name__): print("initial holed polygon tree") poly_tree.tycat() poly_tree.prune(milling_radius) if __debug__: if is_module_debugged(__name__): print("pruned holed polygon tree") poly_tree.tycat() poly_tree.normalize_polygons() poly_tree.compress() if __debug__: if is_module_debugged(__name__): print("compressed polygons") tycat(*list( reversed([ n.content.polygon for n in poly_tree.breadth_first_exploration() if n.content is not None ]))) return poly_tree
def _offset_polygons(poly_tree, carving_radius): if __debug__: if is_module_debugged(__name__): print("building pockets tree from") poly_tree.tycat() # start with children subtrees = [] for child in poly_tree.children: pockets = _offset_polygons(child, carving_radius) for pocket in pockets: pocket.copy_translations(child) subtrees.extend(pockets) holed_poly = poly_tree.content if holed_poly is not None: # now, offset ourselves (if we are not root) polygons = list(holed_poly.holes) polygons.append(holed_poly.polygon) pockets = offset_holed_polygon(carving_radius, *polygons) if __debug__: if is_module_debugged(__name__): print("offsetting") tycat(polygons) print("into") tycat(pockets) return _build_offsetted_tree(pockets, subtrees) else: root = PocketTree() root.children = subtrees return root
def __init__(self, file_name): self.heights_hash = CoordinatesHash(wanted_precision=5) self.facets = [] self.bounding_box = BoundingBox.empty_box(3) if __debug__: if is_module_debugged(__name__): print('loading stl file') self.parse_stl(file_name) if __debug__: if is_module_debugged(__name__): print('stl file loaded')
def build_paths_tree(milling_radius, pockets): """ transform pockets tree into paths tree. """ if __debug__: if is_module_debugged(__name__): print("building paths tree") paths = PathTree.build(pockets, milling_radius) if __debug__: if is_module_debugged(__name__): paths.tycat() return paths
def build_polygons_tree(milling_radius, slices_polygons): """ turn slices into polygons tree. """ if __debug__: if is_module_debugged(__name__): print("building polygon tree") tree = PolygonTree.build(milling_radius, slices_polygons) if __debug__: if is_module_debugged(__name__): tree.tycat() return tree
def build_pockets_tree(milling_radius, tree): """ transform polygons tree into pockets tree. """ if __debug__: if is_module_debugged(__name__): print("building pockets tree") pockets = PocketTree.build(tree, milling_radius) if pockets.is_empty(): print("nothing left : milling radius is too high !") sys.exit() if __debug__: if is_module_debugged(__name__): pockets.tycat() return pockets
def junction_points(self, inner_envelope): """ return couple of points interfering. first one on outer envelope, other on inner enveloppe. first one is furthest possible interference point in outer enveloppe """ points_couples = [] for our_path in self.paths: for envelope_path in inner_envelope.paths: interferences = our_path.interferences_with(envelope_path) if __debug__: if is_module_debugged(__name__) and interferences: print("interferences") tycat(self, inner_envelope, our_path.path, envelope_path.path, interferences) for i in interferences: points_couples.append((our_path.project(i), envelope_path.project(i))) if not points_couples: return None, None if self.inside_content.endpoints[0] < self.inside_content.endpoints[1]: last_couple = max(points_couples, key=lambda c: c[0]) else: last_couple = min(points_couples, key=lambda c: c[0]) return last_couple
def min_spanning_tree(graph): """ prim minimum spanning tree algorithm. """ start = graph.get_any_vertex() heap = [] _add_edges_in_heap(heap, start) reached_vertices = {} reached_vertices[start] = True added_edges = [] limit = graph.vertices_number - 1 # stop after this many edges while heap: if len(added_edges) == limit: if __debug__: if is_module_debugged(__name__): print("spanning tree") tycat(graph, added_edges) return added_edges edge = heappop(heap) destination = edge.vertices[1] if destination not in reached_vertices: added_edges.append(edge) reached_vertices[destination] = True _add_edges_in_heap(heap, destination) print("added", len(added_edges), "need", limit) tycat(graph, added_edges) raise Exception("not enough edges")
def uncompress(self, translation): """ initialize an uncompressed_tree out of a compressed one. """ if self.content is not None: translated_content = self.content.translate(translation) translated_pocket = self.old_pocket.translate(translation) new_node = PathTree(translated_content, translated_pocket) else: new_node = PathTree() # generate children for child in self.children: for child_translation in child.translations: new_translation = child_translation + translation new_node.children.append(child.uncompress(new_translation)) if __debug__: if is_module_debugged(__name__): if self.content is None: # toplevel node print("decompressed path tree") new_node.tycat() return new_node
def split_at(self, points): """ split path at given points. returns list of same type objects ; orientation is kept ; assumes points are on path ; input points can be duplicated but no output paths are. example: points = [Point([c, c]) for c in range(4)] segment = Segment([points[0], points[3]]) small_segments = segment.split_at(points) # small_segments contains segments (0,0) -- (1,1) (1,1) -- (2,2) (2,2) -- (3,3) (3,3) -- (4,4) """ # pylint: disable=no-member points = set([p for a in (self.endpoints, points) for p in a]) sorted_points = sorted(points, key=self.distance_from_start) paths = [] for point1, point2 in zip(sorted_points[:-1], sorted_points[1:]): path_chunk = self.copy() path_chunk.endpoints[0] = point1.copy() path_chunk.endpoints[1] = point2.copy() paths.append(path_chunk) if __debug__: if is_module_debugged(__name__): print("splitting path:") tycat(self, *paths) return paths
def _odd_segments_on_line(self, line_hash): """ sweeps through line of aligned segments. keeping the ones we want """ # associate #starting - #ending segments to each point self.counters = defaultdict(int) segments = self.lines[line_hash] self._compute_points_and_counters(segments) # now iterate through each point # we record on how many segments we currently are previously_on = 0 # we record where interesting segment started previous_point = None odd_segments = [] for point in self.sorted_points: now_on = previously_on now_on += self.counters[point] if previously_on % 2 == 1: if now_on % 2 == 0: odd_segments.append(Segment([previous_point, point])) else: previous_point = point previously_on = now_on if __debug__: if is_module_debugged(__name__): tycat(self.segments, odd_segments) return odd_segments
def __init__(self, inside_content, distance): """ inflates path by given distance """ self.distance = distance self.inside_content = inside_content # for debug only if isinstance(inside_content, Pocket): inside = inside_content.paths else: inside = (inside_content, inside_content.reverse()) try: self.paths = [] # just follow path, moving away raw_paths = [DisplacedPath.displace(p, distance) for p in inside] # and then reconnecting everything. for path1, path2 in all_two_elements(raw_paths): self.paths.extend(path1.reconnect(path2, distance)) except: print("failed compute envelope for", self.inside_content) tycat(self.inside_content, [p.path for p in raw_paths]) raise if __debug__: if is_module_debugged(__name__): print("inflating") tycat(self)
def find_eulerian_cycle(graph): """ eulerian cycle classical algorithm. requires all degrees to be even. """ # we loop finding cycles until graph is empty possible_starts = {} # where to search for a new cycle start_vertex = graph.get_any_vertex() # we constrain possible starting points # to be only a previous cycles # this will enable easier merging of all cycles possible_starts[start_vertex] = start_vertex.degree() # we just need to remember for each cycle its starting point cycle_starts = defaultdict(list) # where do found cycles start first_cycle = None while not graph.is_empty(): cycle = _find_cycle(graph, possible_starts) if __debug__: if is_module_debugged(__name__): print("found new cycle") tycat(graph, cycle) if first_cycle is None: first_cycle = cycle else: cycle_start = cycle[0].vertices[0] cycle_starts[cycle_start].append(cycle) final_cycle = _fuse_cycles(first_cycle, cycle_starts) return final_cycle
def offset_to_elementary_paths(radius, polygons): """ compute all paths obtained when offsetting. handle overlaps and intersections and return a set of elementary paths ready to be used for rebuilding pockets. """ # offset each polygon pockets = [_offset(radius, p) for p in polygons] # remove overlapping segments for pocket1, pocket2 in combinations(pockets, r=2): pocket1.remove_overlap_with(pocket2) # compute intersections intersections = defaultdict(list) # to each path a list of intersections for pocket1, pocket2 in combinations(pockets, r=2): pocket1.intersections_with(pocket2, intersections) # compute self intersections and generate elementary paths for pocket in pockets: pocket.self_intersections(intersections) pocket.split_at(intersections) paths = [] for pocket in pockets: paths.extend(pocket.paths) if __debug__: if is_module_debugged(__name__): print("elementary paths") tycat(paths) return paths
def join_raw_segments(self, raw_segments): """ reconnect all parallel segments. """ for neighbouring_tuples in all_two_elements(raw_segments): first_segment, second_segment = [p[0] for p in neighbouring_tuples] end = first_segment.endpoints[1] start = second_segment.endpoints[0] if end.is_almost(start): first_segment = Segment([first_segment.endpoints[0], start]) else: # original point connecting the original segments center_point = neighbouring_tuples[0][1].endpoints[1] # add arc try: binding = Arc(self.radius, [end, start], center_point) binding.adjust_center() binding.correct_endpoints_order() except: print("failed joining segments") tycat(self.polygon, center_point, first_segment, second_segment) raise self.edge.append(first_segment) self.edge.append(binding) if __debug__: if is_module_debugged(__name__): print("joined segments") tycat(self.polygon, self.edge)
def _create_internal_edges_in_slice(graph, milling_y, vertices): """ move on slice line. when inside add edge. """ current_position = Position(milling_y, outside=True) for edge in _horizontal_edges(vertices): current_position.update(edge) if current_position.is_inside(): edge.add_directly_to_graph() if __debug__: if is_module_debugged(__name__): print("adding horizontal edge", str(current_position)) tycat(graph, edge) else: if __debug__: if is_module_debugged(__name__): print("not adding horizontal edge", str(current_position)) tycat(graph, edge)
def build_inclusion_tree(polygons): """ turn a set of polygons hashed by height into a polygon tree. """ builder = InclusionTreeBuilder(polygons) if __debug__: if is_module_debugged(__name__): print("inclusion tree") builder.tree.tycat() return builder.tree
def _create_vertices(milled_pocket, milling_diameter, built_graph): # first cut by horizontal lines spaced by milling_diameter split_pocket = milled_pocket.split_at_milling_points(milling_diameter) # ok, now create graph, each segment point becomes a vertex # and we add all external edges for path in split_pocket.paths: built_graph.add_edge(path, frontier_edge=True) if __debug__: if is_module_debugged(__name__): print("created vertices") tycat(built_graph)
def compute_milling_path(stl_file, slice_size, milling_radius): """ main procedure. loads stl file ; cuts in slices of thickness 'slice_size' ; compute polygons and offset them with milling radius ; returns global milling path """ if __debug__: if is_module_debugged(__name__): print("computing path ; thickness is", slice_size, "radius is", milling_radius) VerticalPath.milling_height = slice_size slices_polygons = slice_stl_file(stl_file, slice_size, milling_radius) tree = build_polygons_tree(milling_radius, slices_polygons) pockets = build_pockets_tree(milling_radius, tree) paths = build_paths_tree(milling_radius, pockets) if __debug__: if is_module_debugged(__name__): print("merging all paths") return paths.global_path(milling_radius)
def intersections_with(self, other): """ return array of intersections with arc or segment. """ if isinstance(other, Segment): intersections = self.intersections_with_segment(other) else: intersections = self.intersections_with_arc(other) if __debug__: if is_module_debugged(__name__): print("intersections are:") tycat(self, other, intersections) return intersections
def _augment_path(graph, start_vertex): # this is a very simple way to find the best augmenting path # it is in no way optimized # and has a complexity of O(n^2) distances, predecessors = bellman_ford(graph, start_vertex) destination = _find_nearest_odd_vertex(graph, start_vertex, distances) current_point = destination if __debug__: if is_module_debugged(__name__): added_edges = [] while current_point != start_vertex: edge = predecessors[current_point.unique_id] edge.add_directly_to_graph() if __debug__: if is_module_debugged(__name__): added_edges.append(edge) previous_point = edge.vertices[0] current_point = previous_point if __debug__: if is_module_debugged(__name__): print("new augmenting path") tycat(graph, added_edges) print("graph is now") tycat(graph)
def execute(self): """ run bentley ottmann """ while self.events: event_point = self.events.pop(0) # remove ending paths self.remove_paths(self.events_data[1][event_point]) self.current_point = event_point if __debug__: if is_module_debugged(__name__): print("current point is now", self.current_point) # add starting paths for starting_path in self.events_data[0][event_point]: self.add_path(starting_path) if __debug__: if is_module_debugged(__name__): self.tycat() return self
def tsp(graph): """ christofides algorithm. careful: this modifies initial graph. """ if __debug__: if is_module_debugged(__name__): print("starting christofides") spanning_tree = min_spanning_tree(graph) path_graph = _adjust_degree(spanning_tree) cycle = find_eulerian_cycle(path_graph) if __debug__: if is_module_debugged(__name__): print("cycle with duplicated vertices") tycat(cycle) cycle = _skip_seen_vertices(cycle) if __debug__: if is_module_debugged(__name__): print("cycle") tycat(cycle) return cycle
def add_path(self, path): """ new path handler. check each time if new polygon. """ polygon_id = path.get_polygon_id() self.current_paths[polygon_id].append(path) if polygon_id not in self.seen_polygons: # this guy is new, categorize it # add it in tree self.add_polygon_in_tree(path) # mark it as seen self.seen_polygons.add(polygon_id) if __debug__: if is_module_debugged(__name__): print("added polygon", id(path.polygon), "( h =", path.height, ")") self.tree.tycat()
def _adjust_degree(spanning_tree): """ return graph obtained after making degrees even. """ left = Graph.subgraph(_odd_degree_vertices(spanning_tree)) make_degrees_even(left) if __debug__: if is_module_debugged(__name__): print("christofides : matching") tycat(left) for edge in left.double_edges(): edge.change_multiplicity(-1) # set multiplicity back to 1 spanning_tree.append(edge) path_graph = Graph() for edge in spanning_tree: objects = [v.bound_object for v in edge.vertices] path_graph.add_edge_between(*objects, edge_path=edge.path) return path_graph
def project(self, point): """ find where point on stored path projects itself on origin. """ if isinstance(self.origin, Point): result = self.origin elif isinstance(self.origin, Segment): result = self.origin.point_projection(point) else: intersections = self.origin.intersections_with_segment( Segment([self.origin.center, point])) assert len(intersections) == 1 result = intersections[0] if __debug__: if is_module_debugged(__name__): print("project from envelope back to original path") tycat(self.path, self.origin, point, result) return result
def build_pockets(self): """ run the algorithm. """ for start_path in self.paths: if start_path in self.marked_paths: continue # skip paths already used try: pocket = self.build_pocket(start_path) except: print("failed building pocket") raise self.pockets.append(pocket) if __debug__: if is_module_debugged(__name__): print("added pocket") tycat(self.paths, pocket) return self.pockets
def offset_holed_polygon(radius, *polygons): """ take a holed polygon and routing radius. remove non accessible surfaces and return a set of disjoint holed pockets. """ paths = offset_to_elementary_paths(radius, polygons) try: pockets = build_pockets(paths) except: tycat(paths, *polygons) raise final_pockets = _merge_included_pockets(pockets) if __debug__: if is_module_debugged(__name__): print("final pockets") tycat(final_pockets) return final_pockets