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 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 build_pocket(self, start_path): """ start with start_path and follow edge building the pocket. """ current_path = [] start_point, current_point = start_path.endpoints current_path.append(start_path) self.marked_paths[start_path] = start_path previous_point = start_point while current_point != start_point: # find where to go next_path = self.find_next_path(current_point, previous_point) if __debug__: if next_path.reverse() == current_path[-1]: tycat(self.paths, current_path, next_path) raise Exception("path going back") current_path.append(next_path) self.marked_paths[next_path] = next_path # continue moving previous_point = current_point current_point = next_path.endpoints[1] return Pocket(current_path)
def animate(self, milling_radius): """ step by step animation for carving the path with given milling radius. """ total_length = self.length() steps_length = total_length / PATH_IMAGES # all paths strings up to now (by height) current_strings = SortedDict() bounding_box = BoundingBox.empty_box(2) # bounding box up to now current_height = 0 current_length = 0 heights_hash = CoordinatesHash() for path in self.elementary_paths: new_length = current_length + path.length() if isinstance(path, VerticalPath): current_height = path.update_height(current_height) current_height = heights_hash.hash_coordinate(current_height) else: envelope = Envelope(path, milling_radius) if current_height not in current_strings: current_strings[current_height] = [] current_strings[current_height].append(envelope.svg_content()) bounding_box.update(envelope.get_bounding_box()) if floor(current_length / steps_length) != \ floor(new_length / steps_length): tycat(*list(reversed(current_strings.values())), bounding_box=bounding_box) current_length = new_length
def tycat(self): """ graphical display for debugging """ # compute intersections intersections = [] for small_intersections in self.intersections.values(): intersections.extend(small_intersections) intersections = list(set(intersections)) # compute current vertical line bbox = BoundingBox.empty_box(2) for path in self.paths: bbox.update(path.get_bounding_box()) ymin, ymax = bbox.limits(1) height = ymax - ymin line_y_min = ymin - height / 20 line_y_max = ymax + height / 20 current_x = self.current_point.coordinates[0] vertical_line = Segment( [Point([current_x, line_y_min]), Point([current_x, line_y_max])]) # display figure tycat(self.paths, intersections, [self.current_point, vertical_line], *self.crossed_paths) print(list(self.key(p) for p in self.crossed_paths))
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 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 _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 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 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 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 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 execute_event(self, event): """ execute start path or end path event """ event_key, event_path = event event_point, event_type = event_key[0:2] if event_type == START_EVENT: self.current_point = event_point self.start_path(event_path) else: self.end_path(event_path) self.current_point = event_point if __debug__: # very slow paths = iter(self.crossed_paths) previous_path = next(paths, None) for path in paths: if self.key(previous_path) >= self.key(path): paths = list(self.crossed_paths) print(paths) print("previous", previous_path, self.key(previous_path)) print("current", path, self.key(path)) tycat(self.current_point, paths, previous_path, path) raise Exception("pb ordre") previous_path = path
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 _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 main(): """ automated tests or nice display """ if len(sys.argv) > 1: print("using seed", sys.argv[1]) tycat(*test(sys.argv[1])) else: for iteration in range(2000): print(iteration) test() ROUNDER2D.clear() print("done")
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 test_one_level(): """ very basic test with all polygons at same height """ outside_square = Polygon.square(0, 0, 10) inner_square = Polygon.square(2, 2, 2) inner_square2 = Polygon.square(6, 6, 2) inner_inner = Polygon.square(2.1, 2.1, 0.3) polygons = {0: [outside_square, inner_square, inner_square2, inner_inner]} tycat(polygons[0]) print("outside square", id(outside_square)) print("inner square (top left)", id(inner_square)) print("inner square (bottom right)", id(inner_square2)) print("inner inner square (inside top left)", id(inner_inner)) tree = build_inclusion_tree(polygons) tree.tycat()
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_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 __tour(): description = "we provide a 'Segment' class encoding oriented segments." example = """ from jimn.point import Point from jimn.segment import Segment from jimn.displayable import tycat segment1 = Segment([Point([0, 0]), Point([5, 5])]) segment2 = Segment([Point([0, 3]), Point([7, 1])]) tycat(segment1, segment2, segment1.intersection_with_segment(segment2)) """ tour("jimn.segment", description, example)
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 _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 test(seconds=None): """ intersect a bunch of random segments and display result. """ display = True if seconds is None: display = False seconds = clock() seed(float(seconds)) paths = [ Segment([ ROUNDER2D.hash_point(Point([random(), random()])), ROUNDER2D.hash_point(Point([random(), random()])) ]) for _ in range(10) ] for _ in range(0): center = ROUNDER2D.hash_point(Point([random(), random()])) radius = 0 while radius < 0.02: radius = random() / 4 points = [ center + ROUNDER2D.hash_point(Point([cos(a), sin(a)]) * radius) for a in (random() * 10, random() * 10) ] paths.append(Arc(radius, points, center).correct_endpoints_order()) # print(",\n ".join([str(s) for s in paths])) if display: tycat(paths) try: intersections = compute_intersections(paths) except: print("seed", seconds) tycat(paths) raise intersections.append(paths) return intersections
def vertical_intersection_at(self, intersecting_x): """return y of lowest intersection given vertical line""" min_point, max_point = sorted(self.endpoints) if is_almost(min_point.get_x(), intersecting_x): return min_point.get_y() elif is_almost(max_point.get_x(), intersecting_x): return max_point.get_y() intersections = \ vline_circle_intersections(intersecting_x, self.center, self.radius) candidates = [ i for i in intersections if self.contains_circle_point(i) ] if __debug__ and not candidates: print(self, intersecting_x, [str(i) for i in intersections]) tycat(self, intersections) raise Exception("no intersections") intersecting_ys = [i.get_y() for i in candidates] return min(intersecting_ys)
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
def _build_offsetted_tree(pockets, subtrees): """ takes a set of trees from sublevel and a set of pockets at current level ; figures out in which pocket each tree is included and rebuilds a global tree """ new_trees = {} for pocket in pockets: new_trees[id(pocket)] = PocketTree(pocket) for node in subtrees: for pocket in pockets: if node.content.is_included_in(pocket): new_trees[id(pocket)].children.append(node) break else: tycat(node.content, *pockets) raise Exception("subtree does not belong here") return list(new_trees.values())
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 run_slicing_events(events, translation_vector): """ executes all events, adding, removing and intersecting facets """ slices = dict() facets = set() for height, event_type, facet in events: if event_type == START: facets.add(facet) elif event_type == END: facets.remove(facet) else: segments = [] for facet in facets: facet.intersect(height, segments, translation_vector) slices[height] = segments if is_module_debugged(__name__): print(height) # print("\n".join(str(f) for f in facets)) tycat(segments) return slices