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 clip(self, center, size): """ clip self in square of given size at given center. """ # TODO move in elementary path points = [ center + Point([-size, -size]), center + Point([size, -size]), center + Point([size, size]), center + Point([-size, size]), ] segments = [Segment([a, b]) for a, b in all_two_elements(points)] intersections = [] for segment in segments: intersection = segment.intersection_with_segment(self) if intersection: intersections.append(intersection) box = BoundingBox.empty_box(2) box.add_point(points[0]) box.add_point(points[2]) for chunk in self.split_at(intersections): middle = (chunk.endpoints[0] + chunk.endpoints[1]) / 2 if box.almost_contains_point(middle): return chunk
def main(): """ launches all benchmars ; logs on stdout """ system("git rev-parse HEAD") start = clock() points = [Point([random(), random()]) for _ in range(100000)] end = clock() print("created", len(points), "points in", end - start) start = clock() rounder = CoordinatesHash(5) for point in points: rounder.hash_point(point) end = clock() print("hashed", len(points), "points in", end - start) start = clock() model = Stl("../test_files/cordoba.stl") model.compute_slices(0.3, Point([0.0, 0.0])) end = clock() print("slicing small file in", end - start) start = clock() model = Stl("../test_files/cordoba-very-large.stl") model.compute_slices(0.1, Point([0.0, 0.0])) end = clock() print("slicing large file in", end - start) start = clock() compute_milling_path("../test_files/cordoba.stl", 0.3, 0.3) end = clock() print("easy instance in", end - start) start = clock() compute_milling_path("../test_files/cordoba-very-large.stl", 0.1, 0.1) end = clock() print("difficult instance in", end - start)
def overlapping_run(size): """ one run, of given size. return time taken. """ paths = [ Segment([Point([0, i]), Point([1, i + 0.5])]) for i in range(size) ] return run(paths)
def get_bounding_box(self): """ bounding box for arc. for now, not tight """ box = BoundingBox.empty_box(2) box.add_point(self.center + Point([self.radius, self.radius])) box.add_point(self.center - Point([self.radius, self.radius])) return box
def non_overlapping_run(size): """ one run, of given size. return time taken. """ paths = [ Segment([Point([2 * i, 0]), Point([2 * i + 1, 0])]) for i in range(size) ] return run(paths)
def horizontal_intersection_at(self, intersecting_y): """ return point on self at given y. precondition : y is valid height in segment. """ (x_1, y_1), (x_2, y_2) = [p.coordinates for p in self.endpoints] if is_almost(x_1, x_2): return Point([x_1, intersecting_y]) else: slope = (y_1 - y_2) / (x_1 - x_2) intersecting_x = (intersecting_y - y_1) / slope + x_1 return Point([intersecting_x, intersecting_y])
def horizontal_intersections_at(self, intersecting_y, xmin, xmax): """ intersections with horizontal line at given y. returns array of points. """ segment = Segment( [Point([xmin, intersecting_y]), Point([xmax, intersecting_y])]) intersections = self.intersections_with_segment(segment) intersections = [ Point([i.get_x(), intersecting_y]) for i in intersections ] return intersections
def quadratic(size): """ n^2 brute force algorithm """ paths = [ Segment([Point([2 * i, 0]), Point([2 * i + 1, 0])]) for i in range(size) ] start_time = clock() for p_1, p_2 in combinations(paths, r=2): p_1.intersections_with(p_2) end_time = clock() return end_time - start_time
def vline_circle_intersections(x, center, radius): """ intersection of circle with vertical line at given x. """ distance = abs(x - center.get_x()) if is_almost(radius, distance): return [Point([x, center.get_y()])] squared_distance = distance * distance squared_radius = radius * radius if squared_distance > squared_radius: return [] y = sqrt(squared_radius - squared_distance) return [Point([x, center.get_y() + y]), Point([x, center.get_y() - y])]
def border_2d(stl_model, margin): """ return 2d enclosing (starting at origin) for given model and margin. """ # build four points xmin, ymin = 0, 0 xmax, ymax = stl_model.dimensions(margin) points = [] points.append(Point([xmin, ymin])) points.append(Point([xmin, ymax])) points.append(Point([xmax, ymax])) points.append(Point([xmax, ymin])) return [Segment([p, q]) for p, q in all_two_elements(points)]
def line_hash(self): """ return unique id of line on which is segment. nearly aligned segments will hash on same value. """ (x_1, y_1), (x_2, y_2) = [p.coordinates for p in self.endpoints] if is_almost(x_1, x_2): return str(LINES_ROUNDER.hash_point(Point([x_1]))) else: slope = (y_2 - y_1) / (x_2 - x_1) height_at_origin = y_1 - slope * x_1 return str( LINES_ROUNDER.hash_point(Point([slope, height_at_origin])))
def square(cls, start_x, start_y, side): """ create a square, horizontally aligned. used in many test scripts as a quick way to get polygons. """ starting_point = Point([start_x, start_y]) points = [ Point([0.0, 0.0]), Point([side, 0.0]), Point([side, side]), Point([0.0, side]), ] points = [p + starting_point for p in points] square_polygon = cls(points) return square_polygon
def _compute_toplevel_tour(self): """ find cycle starting at origin and passing through one point of each toplevel pocket. return list of 2d points. this will also sort all children by order of visit of the tour and change each cycle starting point as the visited point. """ origin = Point([0, 0]) graph = Graph() children = {} # record to which child each point belongs for child in self.children: end = nearest_point(child.old_pocket, origin) children[end] = child graph.add_edge_between(origin, child.old_pocket, Segment([origin, end])) for child1, child2 in combinations(self.children, 2): start, end = nearest_points(child1.old_pocket, child2.old_pocket) children[start] = child1 children[end] = child2 graph.add_edge_between(child1.old_pocket, child2.old_pocket, Segment([start, end])) cycle = tsp(graph) tour = self._convert_cycle_to_tour(cycle, children, origin) return tour
def translation_vector(self, margin): """ return translation vector to apply to all points to have border min point in (0, 0). """ xmin, ymin = self.bounding_box.min_coordinates[0:2] return Point([margin - xmin, margin - ymin])
def translate(self, translation): """ translates the whole pocket by a given translation vector. returns new pocket if obtained pocket is different and same pocket if translation vector is (0,0) """ if translation.is_almost(Point([0, 0])): return self return Pocket([p.translate(translation) for p in self.paths])
def translate(self, translation): """ translates the whole path by a given translation vector. returns new path if obtained path is different and same path if translation vector is (0,0) """ if translation.is_almost(Point([0, 0])): return self return Path([p.translate(translation) for p in self.elementary_paths])
def parallel_segment(self, distance, side=1): """ return segment parallel to self at given distance, on given side. this operation might lead to precision problems. keep in mind that if you have three nearly aligned points, by taking parallel segments you might obtain four non-aligned points. """ angle = self.endpoints[0].angle_with(self.endpoints[1]) angle += side * pi / 2 displacement = Point([distance * cos(-angle), distance * sin(-angle)]) return Segment([p + displacement for p in self.endpoints])
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 save_svg_content(self, display, color): """ svg code for displaying box. pre-requisite: 2d box """ points = [ Point(list(self.min_coordinates)), Point(list(self.max_coordinates)) ] coordinates = [ display.convert_coordinates(p.coordinates) for p in points ] stroke_width = display.stroke_width() for indices in (((0, 0), (0, 1)), ((0, 1), (1, 1)), ((1, 1), (1, 0)), ((1, 0), (0, 0))): display.write("<line x1=\"{}\" y1=\"{}\"\ x2=\"{}\" y2=\"{}\"".format( coordinates[indices[0][0]][0], coordinates[indices[0][1]][1], coordinates[indices[1][0]][0], coordinates[indices[1][1]][1])) display.write(" stroke-width=\"{}\" stroke=\"{}\"\ opacity=\"0.5\"/>\n".format(stroke_width, color))
def circles_intersections(c1, c2, r1, r2): """ intersect two circles with given centers and radiuses. return points array. """ d = c1.distance_to(c2) if is_almost(d, 0): return [] # common center x1, y1, x2, y2 = [c for p in (c1, c2) for c in p.coordinates] if is_almost(r1, r2): l = d/2 else: l = (r1 * r1 - r2 * r2) / (2 * d) + d/2 if is_almost(r1, l): # only one intersection i = Point([ l/d * (x2 - x1) + x1, l/d * (y2 - y1) + y1 ]) return [i] else: if r1 < l: return [] # too far away if abs(r1) < abs(l): return [] else: h = sqrt(r1 * r1 - l * l) points = [ Point([ l/d * (x2 - x1) + h/d * (y2 - y1) + x1, l/d * (y2 - y1) - h/d * (x2 - x1) + y1 ]), Point([ l/d * (x2 - x1) - h/d * (y2 - y1) + x1, l/d * (y2 - y1) + h/d * (x2 - x1) + y1 ]) ] return points
def hash_point(self, point): """ add a point to the hash. if a nearby point is already hashed do not hash but return nearby point. """ if point in self.fast_hash: return point new_coordinates = [self.hash_coordinate(c, i) for i, c in enumerate(point.coordinates)] new_point = Point(new_coordinates) self.fast_hash.add(new_point) return new_point
def __adjust_point(self, milling_diameter): """ if point is close enough from a milling height, return new point exactly at milling height. else return point. """ point_y = self.get_y() above_height = milling_diameter * floor(point_y / milling_diameter) below_height = milling_diameter * ceil(point_y / milling_diameter) for height in (above_height, below_height): if is_almost(point_y, height): return Point([self.get_x(), height]) return self
def global_path(self, milling_radius): """ flatten the tree into final path. """ # switch back to real tree self = self.uncompress(Point([0, 0])) # start by computing toplevel tour if len(self.children) > 20: toplevel_tour = self._compute_toplevel_tour_fast() else: toplevel_tour = self._compute_toplevel_tour() # now, process all subtrees for child in self.children: child.merge_paths(milling_radius) return self._merge_toplevel(toplevel_tour)
def _parse_ascii_points(self, points_strings): points = [] for point_string in points_strings: matches = re.search( r"^\s*(-?\d+(\.\d+)?)\s+(-?\d+(\.\d+)?)\s+(-?\d+(\.\d+)?)", point_string) coordinates = [ float(matches.group(1)), float(matches.group(3)), float(matches.group(5)) ] coordinates[2] = self.heights_hash.hash_coordinate(coordinates[2]) point = Point(coordinates) self.bounding_box.add_point(point) points.append(point) self.facets.append(Facet(points))
def compute_arc_centers(radius, points): """ return list of possible centers for an arc of given radius going through given points. """ # take points[0] as origin point2 = points[1] - points[0] # find bisector middle = point2/2 bisector_point = middle + point2.perpendicular_vector() # intersect with circle at origin intersections = line_circle_intersections( [middle, bisector_point], Point([0, 0]), radius ) assert len(intersections) == 2, "invalid arc" centers = [points[0] + i for i in intersections] return centers
def horizontal_split(self): """ split ourselves on arcs such that for any given x, each arc only has one point. return array of sub-arcs. """ extreme_points = [ self.center + Point([f * self.radius, 0]) for f in (-1, 1) ] for extremum in extreme_points: if self.endpoints[0].is_almost(extremum) or\ self.endpoints[1].is_almost(extremum): continue if self.contains(extremum): return [ Arc(self.radius, [self.endpoints[0], extremum], self.center, self.reversed_direction), Arc(self.radius, [extremum, self.endpoints[1]], self.center, self.reversed_direction) ] return [self]
def inside_point_not_on(self, x_hash): """ return a random point STRICTLY inside pocket whose x coordinate is not in given hash. """ # take a x coordinate not in given hash x_coordinates = [ p.coordinates[0] for path in self.paths for p in path.endpoints ] xmin = min(x_coordinates) xmax = max(x_coordinates) chosen_x = None while chosen_x is None or x_hash.contains_coordinate(chosen_x): factor = random() chosen_x = xmin * factor + (1 - factor) * xmax # now, intersect vertically # to figure which y ranges are on the inside intersections = sorted(self.vertical_intersections(chosen_x)) # take point in first range chosen_y = (intersections[0] + intersections[1]) / 2 return Point([chosen_x, chosen_y])
def sweeping_key(self, current_x): """ return key used in sweeping line algorithms for comparing paths. key is : intersection with vline at current_x, direction angle leaving intersection (following tangent), final angle (towards destination) """ if __debug__: x_coordinates = sorted([p.get_x() for p in self.endpoints]) if not x_coordinates[0] <= current_x <= x_coordinates[1]: print("path is", self, "current x is:", current_x) raise Exception("non comparable paths in tree") # start by finding the path's y for current x point_key = Point( [current_x, self.vertical_intersection_at(current_x)]) if self.endpoints[0] < self.endpoints[1]: first_point, last_point = self.endpoints else: last_point, first_point = self.endpoints terminal_angle = (pi / 2 - first_point.angle_with(last_point)) % pi # we need to round so that segments tangent to some arcs # will get exact same angles # TODO: why 10 ? outgoing_angle = round((pi - self.center.angle_with(point_key)) % pi, 10) angles = (outgoing_angle, terminal_angle) # now just reverse angles based on direction if last_point.is_almost(point_key): full_key = (point_key, -angles[0], -angles[1]) else: full_key = (point_key, angles[0], angles[1]) return full_key
def __init__(self, paths, cut_arcs=False): """ prepare for sweeping line algorithm on a set of paths. """ # sweeping line algorithms are based on events # each event is meeting a new point self.events = SortedListWithKey( key=lambda t: t[1].sweeping_key(t[0].get_x())) if cut_arcs: self._add_cut_arcs(paths) else: for path in paths: self.events.add((min(path.endpoints), path)) self.current_point = None # current point in sweeping movement # visible paths at current_point # we put a sentinel as root node whose key values at +infinity self.crossed_paths = Treap( (Point([float("+inf"), float("+inf")]), 0, 0), root_node=True) self.crossed_paths.set_comparer(self) self._run()