def lineBetweenTwoPlanes(line, planeInf, planeSup): """\ returns a tuple that contains : - the case encountered (aa, ab, ac, bb, bc, cc) - the line between the two planes (cases ab-c et bb-c) else None """ p1, p2 = (line.p1, line.p2) if line.p1.z <= line.p2.z else (line.p2, line.p1) direction = p2.sub(p1) if p1.z <= planeInf.p.z: if p2.z <= planeInf.p.z: return ('aa', None) elif p2.z < planeSup.p.z: sec, l = planeInf.intersect_point(direction, p1) return ('ab', Line(sec, p2)) elif p2.z >= planeSup.p.z: sec1, l1 = planeInf.intersect_point(direction, p1) sec2, l2 = planeSup.intersect_point(direction, p2) return ('ac', Line(sec1, sec2)) elif p1.z < planeSup.p.z: if p2.z < planeSup.p.z: return ('bb', line) elif p2.z >= planeSup.p.z: sec, l = planeSup.intersect_point(direction, p2) return ('bc', Line(p2, sec)) elif p1.z >= planeSup.p.z: return ('cc', None)
def reset_cache(self): self.minx = min(self.p1[0], self.p2[0], self.p3[0]) self.miny = min(self.p1[1], self.p2[1], self.p3[1]) self.minz = min(self.p1[2], self.p2[2], self.p3[2]) self.maxx = max(self.p1[0], self.p2[0], self.p3[0]) self.maxy = max(self.p1[1], self.p2[1], self.p3[1]) self.maxz = max(self.p1[2], self.p2[2], self.p3[2]) self.e1 = Line(self.p1, self.p2) self.e2 = Line(self.p2, self.p3) self.e3 = Line(self.p3, self.p1) # calculate normal, if p1-p2-pe are in clockwise order if self.normal is None: self.normal = pnormalized( pcross(psub(self.p3, self.p1), psub(self.p2, self.p1))) if not len(self.normal) > 3: self.normal = (self.normal[0], self.normal[1], self.normal[2], 'v') self.center = pdiv(padd(padd(self.p1, self.p2), self.p3), 3) self.plane = Plane(self.center, self.normal) # calculate circumcircle (resulting in radius and middle) denom = pnorm(pcross(psub(self.p2, self.p1), psub(self.p3, self.p2))) self.radius = (pdist(self.p2, self.p1) * pdist(self.p3, self.p2) * pdist(self.p3, self.p1)) / (2 * denom) self.radiussq = self.radius**2 denom2 = 2 * denom * denom alpha = pdist_sq(self.p3, self.p2) * pdot(psub( self.p1, self.p2), psub(self.p1, self.p3)) / denom2 beta = pdist_sq(self.p1, self.p3) * pdot(psub( self.p2, self.p1), psub(self.p2, self.p3)) / denom2 gamma = pdist_sq(self.p1, self.p2) * pdot(psub( self.p3, self.p1), psub(self.p3, self.p2)) / denom2 self.middle = (self.p1[0] * alpha + self.p2[0] * beta + self.p3[0] * gamma, self.p1[1] * alpha + self.p2[1] * beta + self.p3[1] * gamma, self.p1[2] * alpha + self.p2[2] * beta + self.p3[2] * gamma)
def test_line(self): """Circle->Line collisions""" func = pycam.Geometry.intersection.intersect_circle_line func_args = [ self._circle["center"], self._circle["axis"], self._circle["radius"], self._circle["radius"]**2 ] # additional arguments: direction, edge """ edge = Line((-1, -1, 4), (5, 5, 4)) coll = func(*(func_args + [(0, 0, -1)] + [edge])) # The collision point seems to be the middle of the line. # This is technically not necessary, but the current algorithm does it this way. self.assert_collision_equal(((1.5, 1.5, 10), (1.5, 1.5, 4), 6), coll) """ """ # line dips into circle edge = Line((4, 1, 3), (10, 1, 5)) coll = func(*(func_args + [(0, 0, -1)] + [edge])) #self.assert_collision_equal(((-1, 1, 10), (-1, 1, 2), 8), coll) self.assert_collision_equal(((5, 1, 10), (5, 1, 3.3333333333), 6.666666666), coll) # horizontally skewed line edge = Line((2, 1, 3), (8, 1, 5)) coll = func(*(func_args + [(0, 0, -1)] + [edge])) #self.assert_collision_equal(((-1, 1, 10), (-1, 1, 2), 8), coll) self.assert_collision_equal(((5, 1, 10), (5, 1, 4), 6), coll) """ # line touches circle edge = Line((10, 10, 4), (5, 1, 4)) coll = func(*(func_args + [(0, 0, -1)] + [edge])) self.assert_collision_equal(((5, 1, 10), (5, 1, 4), 6), coll) # no collision edge = Line((10, 10, 4), (5.001, 1, 4)) coll = func(*(func_args + [(0, 0, -1)] + [edge])) self.assert_collision_equal((None, None, INFINITE), coll)
def get_lines_layer(lines, z, last_z=None, step_width=None, milling_style=MillingStyle.CONVENTIONAL): get_proj_point = lambda proj_point: (proj_point[0], proj_point[1], z) projected_lines = [] for line in lines: if (last_z is not None) and (last_z < line.minz): # the line was processed before continue elif line.minz < z < line.maxz: # Split the line at the point at z level and do the calculation # for both point pairs. factor = (z - line.p1[2]) / (line.p2[2] - line.p1[2]) plane_point = padd(line.p1, pmul(line.vector, factor)) if line.p1[2] < z: p1 = get_proj_point(line.p1) p2 = line.p2 else: p1 = line.p1 p2 = get_proj_point(line.p2) projected_lines.append(Line(p1, plane_point)) yield Line(plane_point, p2) elif line.minz < last_z < line.maxz: plane = Plane((0, 0, last_z), (0, 0, 1, 'v')) cp = plane.intersect_point(line.dir, line.p1)[0] # we can be sure that there is an intersection if line.p1[2] > last_z: p1, p2 = cp, line.p2 else: p1, p2 = line.p1, cp projected_lines.append(Line(p1, p2)) else: if line.maxz <= z: # the line is completely below z projected_lines.append( Line(get_proj_point(line.p1), get_proj_point(line.p2))) elif line.minz >= z: projected_lines.append(line) else: _log.warn( "Unexpected condition 'get_lines_layer': %s / %s / %s / %s", line.p1, line.p2, z, last_z) # process all projected lines for line in projected_lines: points = [] if step_width is None: points.append(line.p1) points.append(line.p2) else: if isiterable(step_width): steps = step_width else: steps = floatrange(0.0, line.len, inc=step_width) for step in steps: next_point = padd(line.p1, pmul(line.dir, step)) points.append(next_point) yield points
def extend_shifted_lines(self): # TODO: improve the code below to handle "holes" properly (neighbours # that disappear due to a negative collision distance - use the example # "SampleScene.stl" as a reference) def get_right_neighbour(group, ref): group_len = len(group) # limit the search for a neighbour for non-closed groups if self.waterlines[group[0]].p1 == self.waterlines[group[-1]].p2: index_range = range(ref + 1, ref + group_len) else: index_range = range(ref + 1, group_len) for index in index_range: line_id = group[index % group_len] if not self.shifted_lines[line_id] is None: return line_id else: return None groups = self._get_groups() for group in groups: index = 0 while index < len(group): current = group[index] current_shifted = self.shifted_lines[current] if current_shifted is None: index += 1 continue neighbour = get_right_neighbour(group, index) if neighbour is None: # no right neighbour available break neighbour_shifted = self.shifted_lines[neighbour] if current_shifted.p2 == neighbour_shifted.p1: index += 1 continue cp, dist = current_shifted.get_intersection( neighbour_shifted, infinite_lines=True) cp2, dist2 = neighbour_shifted.get_intersection( current_shifted, infinite_lines=True) # TODO: add an arc (composed of lines) for a soft corner (not # required, but nicer) if dist < epsilon: self.shifted_lines[current] = None index -= 1 elif dist2 > 1 - epsilon: self.shifted_lines[neighbour] = None else: self.shifted_lines[current] = Line(current_shifted.p1, cp) self.shifted_lines[neighbour] = Line( cp, neighbour_shifted.p2) index += 1
def GenerateToolPathLinePush(self, pa, line, z, previous_z, draw_callback=None): if previous_z <= line.minz: # the line is completely above the previous level pass elif line.minz < z < line.maxz: # Split the line at the point at z level and do the calculation # for both point pairs. factor = (z - line.p1.z) / (line.p2.z - line.p1.z) plane_point = line.p1.add(line.vector.mul(factor)) self.GenerateToolPathLinePush(pa, Line(line.p1, plane_point), z, previous_z, draw_callback=draw_callback) self.GenerateToolPathLinePush(pa, Line(plane_point, line.p2), z, previous_z, draw_callback=draw_callback) elif line.minz < previous_z < line.maxz: plane = Plane(Point(0, 0, previous_z), Vector(0, 0, 1)) cp = plane.intersect_point(line.dir, line.p1)[0] # we can be sure that there is an intersection if line.p1.z > previous_z: p1, p2 = cp, line.p2 else: p1, p2 = line.p1, cp self.GenerateToolPathLinePush(pa, Line(p1, p2), z, previous_z, draw_callback=draw_callback) else: if line.maxz <= z: # the line is completely below z p1 = Point(line.p1.x, line.p1.y, z) p2 = Point(line.p2.x, line.p2.y, z) elif line.minz >= z: p1 = line.p1 p2 = line.p2 else: log.warn("Unexpected condition EC_GTPLP: %s / %s / %s / %s" % \ (line.p1, line.p2, z, previous_z)) return # no model -> no possible obstacles # model is completely below z (e.g. support bridges) -> no obstacles relevant_models = [m for m in self.models if m.maxz >= z] if not relevant_models: points = [p1, p2] elif self.physics: points = get_free_paths_ode(self.physics, p1, p2) else: points = get_free_paths_triangles(relevant_models, self.cutter, p1, p2) if points: for point in points: pa.append(point) if draw_callback: draw_callback(tool_position=points[-1], toolpath=pa.paths)
def get_lines(self): """ Caching is necessary to avoid constant recalculation for visualization. """ if self._lines_cache is None: # recalculate the line cache lines = [] for index in range(len(self._points) - 1): lines.append(Line(self._points[index], self._points[index + 1])) # Connect the last point with the first only if the polygon is # closed. if self.is_closed: lines.append(Line(self._points[-1], self._points[0])) self._lines_cache = lines return self._lines_cache[:]
def test_get_offset_polygons_truncated_square_inside_small_offset(): """This tests a "truncated square", which is a square with the corners shaved off, and an inside offset that's small compared to the corner chamfers.""" # 'expected_inside_p' is the expected offset polygon with a *negative* # offset, so it's inside the input polygon. lines = (Line((1.4142135623730951, 1.0, 0.0), (8.585786437626904, 1.0, 0.0)), Line((8.585786437626904, 1.0, 0.0), (9.0, 1.4142135623730951, 0.0)), Line((9.0, 1.4142135623730951, 0.0), (9.0, 8.585786437626904, 0.0)), Line((9.0, 8.585786437626904, 0.0), (8.585786437626904, 9.0, 0.0)), Line((8.585786437626904, 9.0, 0.0), (1.4142135623730951, 9.0, 0.0)), Line((1.4142135623730951, 9.0, 0.0), (1.0, 8.585786437626904, 0.0)), Line((1.0, 8.585786437626904, 0.0), (1.0, 1.4142135623730951, 0.0)), Line((1.0, 1.4142135623730951, 0.0), (1.4142135623730951, 1.0, 0.0))) expected_inside_p = Polygon() for line in lines: expected_inside_p.append(line) output_p = truncated_square_p.get_offset_polygons(-1) print("get_offset_polygons() returned:") for p in output_p: print(str(p)) assert (len(output_p) == 1) assert_polygons_are_identical(output_p[0], expected_inside_p)
def filter_toolpath(self, toolpath): new_path = [] last_pos = None optional_moves = [] for move_type, args in toolpath: if move_type in (MOVE_STRAIGHT, MOVE_STRAIGHT_RAPID): if last_pos: # find all remaining pieces of this line inner_lines = [] for polygon in self.settings["polygons"]: inner, outer = polygon.split_line(Line(last_pos, args)) inner_lines.extend(inner) # turn these lines into moves for line in inner_lines: if pdist(line.p1, last_pos) > epsilon: new_path.append((MOVE_SAFETY, None)) new_path.append((move_type, line.p1)) else: # we continue were we left if optional_moves: new_path.extend(optional_moves) optional_moves = [] new_path.append((move_type, line.p2)) last_pos = line.p2 optional_moves = [] # finish the line by moving to its end (if necessary) if pdist(last_pos, args) > epsilon: optional_moves.append((MOVE_SAFETY, None)) optional_moves.append((move_type, args)) last_pos = args elif move_type == MOVE_SAFETY: optional_moves = [] else: new_path.append((move_type, args)) return new_path
def mergeRiddanceLines(lines): """\ returns not-layered part of the lines lines must be aligned """ if len(lines) <= 1: return lines points = {} for line in lines: if line.p1 > line.p2: line.p1, line.p2 = line.p2, line.p1 if line.p1 not in points: points[line.p1] = 1 else: points[line.p1] += 1 if line.p2 not in points: points[line.p2] = -1 else: points[line.p2] -= 1 order = sorted(points.keys()) counter = 0 start = None uniques = [] for point in order: counter += points[point] if counter == 1 and start is None: start = point elif counter != 1 and start is not None: if cmp(start, point): uniques.append(Line(start, point)) start = None return uniques
def get_outside_lines(poly1, poly2): result = [] for line in poly1.get_lines(): collisions = [] for o_line in poly2.get_lines(): cp, dist = o_line.get_intersection(line) if (cp is not None) and (0 < dist < 1): collisions.append((cp, dist)) # sort the collisions according to the distance collisions.append((line.p1, 0)) collisions.append((line.p2, 1)) collisions.sort(key=lambda collision: collision[1]) for index in range(len(collisions) - 1): p1 = collisions[index][0] p2 = collisions[index + 1][0] if pdist(p1, p2) < epsilon: # ignore zero-length lines continue # Use the middle between p1 and p2 to check the # inner/outer state. p_middle = pdiv(padd(p1, p2), 2) p_inside = (poly2.is_point_inside(p_middle) and not poly2.is_point_on_outline(p_middle)) if not p_inside: result.append(Line(p1, p2)) return result
def inside(self) : p1 = self.get_center() p2 = Point(Box.grid.minx-1, p1.y, p1.z) # TODO: implement algo to find nearest border escaping_line = Line(p1, p2) cuts = 0 box = self lines = set() while box.x != 0 : box = box.get_neighbour(-1, 0, 0) for line in box.lines : if line.id not in lines : cut, d = escaping_line.get_intersection(line) if cut is not None : cuts += 1 lines.add(line.id) return cuts%2
def test_get_offset_polygons_square_inside(): # 'expected_inside_p' is the expected offset polygon with a *negative* # offset, so it's inside the input polygon. lines = (Line((1, 1, 0), (9, 1, 0)), Line( (9, 1, 0), (9, 9, 0)), Line((9, 9, 0), (1, 9, 0)), Line((1, 9, 0), (1, 1, 0))) expected_inside_p = Polygon() for line in lines: expected_inside_p.append(line) output_p = square_p.get_offset_polygons(-1) print("get_offset_polygons() returned:") for p in output_p: print(str(p)) assert (len(output_p) == 1) assert_polygons_are_identical(output_p[0], expected_inside_p)
def split_line(self, line): outer = [] inner = [] # project the line onto the polygon's plane proj_line = self.plane.get_line_projection(line) intersections = [] for pline in self.get_lines(): cp, d = proj_line.get_intersection(pline) if cp: intersections.append((cp, d)) # sort the intersections by distance intersections.sort(key=lambda collision: collision[1]) intersections.insert(0, (proj_line.p1, 0)) intersections.append((proj_line.p2, 1)) def get_original_point(d): return padd(line.p1, pmul(line.vector, d)) for index in range(len(intersections) - 1): p1, d1 = intersections[index] p2, d2 = intersections[index + 1] if p1 != p2: middle = pdiv(padd(p1, p2), 2) new_line = Line(get_original_point(d1), get_original_point(d2)) if self.is_point_inside(middle): inner.append(new_line) else: outer.append(new_line) return (inner, outer)
def get_line(i1, i2): a = list(coords[i1 % 4]) b = list(coords[i2 % 4]) # the contour points of the model will always be at level zero a[2] = self.z_level b[2] = self.z_level return Line(a, b)
def discretise_line( self, line ): # TODO: OPTIMISER !!!!! (pas de boucle, un seul cas avec 2 passages, calcul de l'intervale sans list.index) # on discrétise les deux extrémités du segment c1, c2 = self.discretiser_point(ligne.p1), self.discretiser_point( ligne.p2) c1.lignes.append(ligne) c2.lignes.append(ligne) if ligne.p1.x == ligne.p2.x and ligne.p1.x in self.rangex: # la ligne appartient au pavage vertical # alors on va ajouter toutes les paires de cases qu'elle rencontre x = self.rangex.index(ligne.p1.x) if c1.y > c2.y: c1, c2 = c2, c1 for y in range(c1.y, c2.y + 1): self.get_case(c1.z, x, y).lignes.append(ligne) elif ligne.p1.y == ligne.p2.y and ligne.p1.y in self.rangey: # la ligne appartient au pavage horizontal y = self.rangey.index(ligne.p1.y) if c1.x > c2.x: c1, c2 = c2, c1 for x in range(c1.x, c2.x + 1): self.get_case(c1.z, x, y).lignes.append(ligne) else: # sinon la ligne est simplement verticale, horizontale ou oblique # alors on va discrétiser toutes les cases traversées # d'abord les intersections verticales ordx = 1 if c1.x > c2.x: c1, c2 = c2, c1 # pour itérer dans le bon sens for x in self.rangex[c1.x + 1:c2.x]: # pour cela on calcule l'intersection de la ligne avec la grille des X sec, d = ligne.get_intersection(Line(Point(x, self.miny, ligne.p1.z), \ Point(x, self.maxy, ligne.p1.z))) if sec is None: # il n'y a pas intersection : les deux lignes sont parallèles et distinctes break # la ligne sera traitée par l'itération verticale # puis on trouve à quelle colonne l'intersection appartient dsec = self.Case.get_emplacement(0, sec.y, 0) # et on discrétise alors la case correspondante self.get_case(c1.z, c1.x + ordx, dsec[1]).lignes.append(ligne) # le [1] correspond à la composante Y de la coordonnée ordx += 1 # puis celles horizontales de la même manière ordy = 1 if c1.y > c2.y: c1, c2 = c2, c1 for y in self.rangey[c1.y + 1:c2.y]: sec, d = ligne.get_intersection(Line(Point(self.minx, y, ligne.p1.z), \ Point(self.maxx, y, ligne.p1.z))) if sec is None: break # la ligne est horizontale, et donc déjà traitée dsec = self.Case.get_emplacement(sec.x, 0, 0) self.get_case(c1.z, dsec[0], c1.y + ordy).lignes.append(ligne) ordy += 1
def intersect_triangle(self, triangle, counter_clockwise=False): """ Returns the line of intersection of a triangle with a plane. "None" is returned, if: - the triangle does not intersect with the plane - all vertices of the triangle are on the plane The line always runs clockwise through the triangle. """ # don't import Line in the header -> circular import from pycam.Geometry.Line import Line collisions = [] for edge, point in ((triangle.e1, triangle.p1), (triangle.e2, triangle.p2), (triangle.e3, triangle.p3)): cp, l = self.intersect_point(edge.dir, point) # filter all real collisions # We don't want to count vertices double -> thus we only accept # a distance that is lower than the length of the edge. if (not cp is None) and (-epsilon < l < edge.len - epsilon): collisions.append(cp) elif (cp is None) and (pdot(self.n, edge.dir) == 0): cp, dist = self.intersect_point(self.n, point) if abs(dist) < epsilon: # the edge is on the plane collisions.append(point) if len(collisions) == 3: # All points of the triangle are on the plane. # We don't return a waterline, as there should be another non-flat # triangle with the same waterline. return None if len(collisions) == 2: collision_line = Line(collisions[0], collisions[1]) # no further calculation, if the line is zero-sized if collision_line.len == 0: return collision_line cross = pcross(self.n, collision_line.dir) if (pdot(cross, triangle.normal) < 0) == bool(not counter_clockwise): # anti-clockwise direction -> revert the direction of the line collision_line = Line(collision_line.p2, collision_line.p1) return collision_line elif len(collisions) == 1: # only one point is on the plane # This waterline (with zero length) should be of no use. return None else: return None
def inside(self): p1 = self.get_center() p2 = Point(Box.grid.minx - 1, p1.y, p1.z) # TODO: implement algo to find nearest border escaping_line = Line(p1, p2) cuts = 0 box = self lines = set() while box.x != 0: box = box.get_neighbour(-1, 0, 0) for line in box.lines: if line.id not in lines: cut, d = escaping_line.get_intersection(line) if cut is not None: cuts += 1 lines.add(line.id) return cuts % 2
def _offset_loops_to_polygons(offset_loops): model = pycam.Geometry.Model.ContourModel() before = None for n_loop, loop in enumerate(offset_loops): lines = [] _log.info("loop #%d has %d lines/arcs", n_loop, len(loop)) for n_segment, item in enumerate(loop): _log.info("%d -> %s", n_segment, item) point, radius = item[:2] point = (point.x, point.y, 0.0) if before is not None: if radius == -1: lines.append(Line(before, point)) _log.info("%d line %s to %s", n_segment, before, point) else: _log.info("%d arc %s to %s r=%f", n_segment, before, point, radius) center, clock_wise = item[2:4] center = (center.x, center.y, 0.0) direction_before = (before[0] - center[0], before[1] - center[1], 0.0) direction_end = (point[0] - center[0], point[1] - center[1], 0.0) angles = [ 180.0 * get_angle_pi((1.0, 0.0, 0.0), (0, 0.0, 0.0), direction, (0.0, 0.0, 1.0), pi_factor=True) for direction in (direction_before, direction_end) ] if clock_wise: angles.reverse() points = get_points_of_arc(center, radius, angles[0], angles[1]) last_p = before for p in points: lines.append(Line(last_p, p)) last_p = p before = point for line in lines: if line.len > epsilon: model.append(line) return model.get_polygons()
def parse_polyline(self, init): params = self._open_sequence_params if init: self._open_sequence = "POLYLINE" self._open_sequence_items = [] key, value = self._read_key_value() while (key is not None) and (key != self.KEYS["MARKER"]): if key == self.KEYS["CURVE_TYPE"]: if value == 8: params["CURVE_TYPE"] = "BEZIER" elif key == self.KEYS["ENTITY_FLAGS"]: if value == 1: if "ENTITY_FLAGS" not in params: params["ENTITY_FLAGS"] = set() params["ENTITY_FLAGS"].add("IS_CLOSED") key, value = self._read_key_value() if key is not None: self._push_on_stack(key, value) else: # closing if ("CURVE_TYPE" in params) and (params["CURVE_TYPE"] == "BEZIER"): self.lines.extend(get_bezier_lines(self._open_sequence_items)) if ("ENTITY_FLAGS" in params) and ("IS_CLOSED" in params["ENTITY_FLAGS"]): # repeat the same polyline on the other side self._open_sequence_items.reverse() self.lines.extend( get_bezier_lines(self._open_sequence_items)) else: points = [p for p, bulge in self._open_sequence_items] for index in range(len(points) - 1): point = points[index] next_point = points[index + 1] if point != next_point: self.lines.append(Line(point, next_point)) if ("ENTITY_FLAGS" in params) and ("IS_CLOSED" in params["ENTITY_FLAGS"]): # repeat the same polyline on the other side self.lines.append(Line(points[-1], points[0])) self._open_sequence_items = [] self._open_sequence_params = {} self._open_sequence = None
def parse_polyline(self, init): start_line = self.line_number params = self._open_sequence_params if init: self._open_sequence = "POLYLINE" self._open_sequence_items = [] key, value = self._read_key_value() while (not key is None) and (key != self.KEYS["MARKER"]): if key == self.KEYS["CURVE_TYPE"]: if value == 8: params["CURVE_TYPE"] = "BEZIER" elif key == self.KEYS["VERTEX_FLAGS"]: if value == 1: params["VERTEX_FLAGS"] = "EXTRA_VERTEX" key, value = self._read_key_value() if not key is None: self._push_on_stack(key, value) else: # closing if ("CURVE_TYPE" in params) and (params["CURVE_TYPE"] == "BEZIER"): self.lines.extend( pycam.Geometry.get_bezier_lines(self._open_sequence_items)) if ("VERTEX_FLAGS" in params) and \ (params["VERTEX_FLAGS"] == "EXTRA_VERTEX"): # repeat the same polyline on the other side self._open_sequence_items.reverse() self.lines.extend( pycam.Geometry.get_bezier_lines( self._open_sequence_items)) else: points = [p for p, bulge in self._open_sequence_items] for index in range(len(points) - 1): point = points[index] next_point = points[index + 1] if point != next_point: self.lines.append(Line(point, next_point)) if ("VERTEX_FLAGS" in params) and (params["VERTEX_FLAGS"] == "EXTRA_VERTEX"): self.lines.append(Line(points[-1], points[0])) self._open_sequence_items = [] self._open_sequence_params = {} self._open_sequence = None
def get_positioned_lines(self, base_point, skew=None): result = [] get_skewed_point = lambda p: (base_point[0] + p[0] + (p[1] * skew / 100.0), base_point[1] + p[1], base_point[2]) for line in self.lines: skewed_p1 = get_skewed_point(line.p1) skewed_p2 = get_skewed_point(line.p2) # Some triplex fonts contain zero-length lines # (e.g. "/" in italict.cxf). Ignore these. if skewed_p1 != skewed_p2: new_line = Line(skewed_p1, skewed_p2) result.append(new_line) return result
def reset_cache(self): self.minx = min(self.p1.x, self.p2.x, self.p3.x) self.miny = min(self.p1.y, self.p2.y, self.p3.y) self.minz = min(self.p1.z, self.p2.z, self.p3.z) self.maxx = max(self.p1.x, self.p2.x, self.p3.x) self.maxy = max(self.p1.y, self.p2.y, self.p3.y) self.maxz = max(self.p1.z, self.p2.z, self.p3.z) self.e1 = Line(self.p1, self.p2) self.e2 = Line(self.p2, self.p3) self.e3 = Line(self.p3, self.p1) # calculate normal, if p1-p2-pe are in clockwise order if self.normal is None: self.normal = self.p3.sub(self.p1).cross(self.p2.sub( \ self.p1)).normalized() if not isinstance(self.normal, Vector): self.normal = self.normal.get_vector() # make sure that the normal has always a unit length self.normal = self.normal.normalized() self.center = self.p1.add(self.p2).add(self.p3).div(3) self.plane = Plane(self.center, self.normal) # calculate circumcircle (resulting in radius and middle) denom = self.p2.sub(self.p1).cross(self.p3.sub(self.p2)).norm self.radius = (self.p2.sub(self.p1).norm \ * self.p3.sub(self.p2).norm * self.p3.sub(self.p1).norm) \ / (2 * denom) self.radiussq = self.radius**2 denom2 = 2 * denom * denom alpha = self.p3.sub(self.p2).normsq \ * self.p1.sub(self.p2).dot(self.p1.sub(self.p3)) / denom2 beta = self.p1.sub(self.p3).normsq \ * self.p2.sub(self.p1).dot(self.p2.sub(self.p3)) / denom2 gamma = self.p1.sub(self.p2).normsq \ * self.p3.sub(self.p1).dot(self.p3.sub(self.p2)) / denom2 self.middle = Point( self.p1.x * alpha + self.p2.x * beta + self.p3.x * gamma, self.p1.y * alpha + self.p2.y * beta + self.p3.y * gamma, self.p1.z * alpha + self.p2.z * beta + self.p3.z * gamma)
def get_plane_projection(self, plane): if plane == self.plane: return self elif pdot(plane.n, self.plane.n) == 0: log.warn("Polygon projection onto plane: orthogonal projection is not possible") return None else: result = Polygon(plane) for line in self.get_lines(): p1 = plane.get_point_projection(line.p1) p2 = plane.get_point_projection(line.p2) result.append(Line(p1, p2)) # check if the projection would revert the direction of the polygon if pdot(plane.n, self.plane.n) < 0: result.reverse_direction() return result
def get_shifted_waterline(up_vector, waterline, cutter_location): # Project the waterline and the cutter location down to the slice plane. # This is necessary for calculating the horizontal distance between the # cutter and the triangle waterline. plane = Plane(cutter_location, up_vector) wl_proj = plane.get_line_projection(waterline) if wl_proj.len < epsilon: return None offset = wl_proj.dist_to_point(cutter_location) if offset < epsilon: return wl_proj # shift both ends of the waterline towards the cutter location shift = cutter_location.sub(wl_proj.closest_point(cutter_location)) # increase the shift width slightly to avoid "touch" collisions shift = shift.mul(1.0 + epsilon) shifted_waterline = Line(wl_proj.p1.add(shift), wl_proj.p2.add(shift)) return shifted_waterline
def crop(self, polygons, callback=None): # collect all existing toolpath lines open_lines = [] for path in self.paths: if path: for index in range(len(path.points) - 1): open_lines.append( Line(path.points[index], path.points[index + 1])) # go through all polygons and add "inner" lines (or parts thereof) to # the final list of remaining lines inner_lines = [] for polygon in polygons: new_open_lines = [] for line in open_lines: if callback and callback(): return inner, outer = polygon.split_line(line) inner_lines.extend(inner) new_open_lines.extend(outer) open_lines = new_open_lines # turn all "inner_lines" into toolpath moves new_paths = [] current_path = Path() if inner_lines: line = inner_lines.pop(0) current_path.append(line.p1) current_path.append(line.p2) while inner_lines: if callback and callback(): return end = current_path.points[-1] # look for the next connected point for line in inner_lines: if line.p1 == end: inner_lines.remove(line) current_path.append(line.p2) break else: new_paths.append(current_path) current_path = Path() line = inner_lines.pop(0) current_path.append(line.p1) current_path.append(line.p2) if current_path.points: new_paths.append(current_path) self.paths = new_paths
def parse_line(self): start_line = self.line_number # the z-level defaults to zero (for 2D models) p1 = [None, None, 0] p2 = [None, None, 0] color = None key, value = self._read_key_value() while (key is not None) and (key != self.KEYS["MARKER"]): if key == self.KEYS["P1_X"]: p1[0] = value elif key == self.KEYS["P1_Y"]: p1[1] = value elif key == self.KEYS["P1_Z"]: p1[2] = value elif key == self.KEYS["P2_X"]: p2[0] = value elif key == self.KEYS["P2_Y"]: p2[1] = value elif key == self.KEYS["P2_Z"]: p2[2] = value elif key == self.KEYS["COLOR"]: color = value else: pass key, value = self._read_key_value() end_line = self.line_number # The last lines were not used - they are just the marker for the next # item. if key is not None: self._push_on_stack(key, value) if (None in p1) or (None in p2): log.warn( "DXFImporter: Incomplete LINE definition between line %d and %d", start_line, end_line) else: if self._color_as_height and (color is not None): # use the color code as the z coordinate p1[2] = float(color) / 255 p2[2] = float(color) / 255 line = Line((p1[0], p1[1], p1[2]), (p2[0], p2[1], p2[2])) if line.p1 != line.p2: self.lines.append(line) else: log.warn( "DXFImporter: Ignoring zero-length LINE (between input line %d and %d): " "%s", start_line, end_line, line)
def parse_polyline(self, init): start_line = self.line_number if init: self._open_sequence = "POLYLINE" self._open_sequence_items = [] key, value = self._read_key_value() while (not key is None) and (key != self.KEYS["MARKER"]): key, value = self._read_key_value() if not key is None: self._push_on_stack(key, value) else: # closing points = self._open_sequence_items for index in range(len(points) - 1): point = points[index] next_point = points[index + 1] if point != next_point: self.lines.append(Line(point, next_point)) self._open_sequence_items = [] self._open_sequence = None
def filter_toolpath(self, toolpath): new_path = [] last_pos = None optional_moves = [] for step in toolpath: if step.action in MOVES_LIST: if last_pos: # find all remaining pieces of this line inner_lines = [] for polygon in self.settings["polygons"]: inner, outer = polygon.split_line( Line(last_pos, step.position)) inner_lines.extend(inner) # turn these lines into moves for line in inner_lines: if pdist(line.p1, last_pos) > epsilon: new_path.append(ToolpathSteps.MoveSafety()) new_path.append( ToolpathSteps.get_step_class_by_action( step.action)(line.p1)) else: # we continue where we left if optional_moves: new_path.extend(optional_moves) optional_moves = [] new_path.append( ToolpathSteps.get_step_class_by_action( step.action)(line.p2)) last_pos = line.p2 optional_moves = [] # finish the line by moving to its end (if necessary) if pdist(last_pos, step.position) > epsilon: optional_moves.append(ToolpathSteps.MoveSafety()) optional_moves.append(step) last_pos = step.position elif step.action == MOVE_SAFETY: optional_moves = [] else: new_path.append(step) return new_path
def get_collision_waterline_of_triangle(model, cutter, up_vector, triangle, z): # TODO: there are problems with "material allowance > 0" plane = Plane(Point(0, 0, z), up_vector) if triangle.minz >= z: # no point of the triangle is below z # try all edges # Case (4) proj_points = [] for p in triangle.get_points(): proj_p = plane.get_point_projection(p) if not proj_p in proj_points: proj_points.append(proj_p) if len(proj_points) == 3: edges = [] for index in range(3): edge = Line(proj_points[index - 1], proj_points[index]) # the edge should be clockwise around the model if edge.dir.cross(triangle.normal).dot(up_vector) < 0: edge = Line(edge.p2, edge.p1) edges.append((edge, proj_points[index - 2])) outer_edges = [] for edge, other_point in edges: # pick only edges, where the other point is on the right side if other_point.sub(edge.p1).cross(edge.dir).dot(up_vector) > 0: outer_edges.append(edge) if len(outer_edges) == 0: # the points seem to be an one line # pick the longest edge long_edge = edges[0][0] for edge, other_point in edges[1:]: if edge.len > long_edge.len: long_edge = edge outer_edges = [long_edge] else: edge = Line(proj_points[0], proj_points[1]) if edge.dir.cross(triangle.normal).dot(up_vector) < 0: edge = Line(edge.p2, edge.p1) outer_edges = [edge] else: # some parts of the triangle are above and some below the cutter level # Cases (2a), (2b), (3a) and (3b) points_above = [plane.get_point_projection(p) for p in triangle.get_points() if p.z > z] waterline = plane.intersect_triangle(triangle) if waterline is None: if len(points_above) == 0: # the highest point of the triangle is at z outer_edges = [] else: if abs(triangle.minz - z) < epsilon: # This is just an accuracy issue (see the # "triangle.minz >= z" statement above). outer_edges = [] elif not [p for p in triangle.get_points() if p.z > z + epsilon]: # same as above: fix for inaccurate floating calculations outer_edges = [] else: # this should not happen raise ValueError(("Could not find a waterline, but " \ + "there are points above z level (%f): " \ + "%s / %s") % (z, triangle, points_above)) else: # remove points that are not part of the waterline points_above = [p for p in points_above if (p != waterline.p1) and (p != waterline.p2)] if len(points_above) == 0: # part of case (2a) outer_edges = [waterline] elif len(points_above) == 1: other_point = points_above[0] dot = other_point.sub(waterline.p1).cross(waterline.dir).dot( up_vector) if dot > 0: # Case (2b) outer_edges = [waterline] elif dot < 0: # Case (3b) edges = [] edges.append(Line(waterline.p1, other_point)) edges.append(Line(waterline.p2, other_point)) outer_edges = [] for edge in edges: if edge.dir.cross(triangle.normal).dot(up_vector) < 0: outer_edges.append(Line(edge.p2, edge.p1)) else: outer_edges.append(edge) else: # the three points are on one line # part of case (2a) edges = [] edges.append(waterline) edges.append(Line(waterline.p1, other_point)) edges.append(Line(waterline.p2, other_point)) edges.sort(key=lambda x: x.len) edge = edges[-1] if edge.dir.cross(triangle.normal).dot(up_vector) < 0: outer_edges = [Line(edge.p2, edge.p1)] else: outer_edges = [edge] else: # two points above other_point = points_above[0] dot = other_point.sub(waterline.p1).cross(waterline.dir).dot( up_vector) if dot > 0: # Case (2b) # the other two points are on the right side outer_edges = [waterline] elif dot < 0: # Case (3a) edge = Line(points_above[0], points_above[1]) if edge.dir.cross(triangle.normal).dot(up_vector) < 0: outer_edges = [Line(edge.p2, edge.p1)] else: outer_edges = [edge] else: edges = [] # pick the longest combination of two of these points # part of case (2a) # TODO: maybe we should use the waterline instead? # (otherweise the line could be too long and thus # connections to the adjacent waterlines are not discovered? # Test this with an appropriate test model.) points = [waterline.p1, waterline.p2] + points_above for p1 in points: for p2 in points: if not p1 is p2: edges.append(Line(p1, p2)) edges.sort(key=lambda x: x.len) edge = edges[-1] if edge.dir.cross(triangle.normal).dot(up_vector) < 0: outer_edges = [Line(edge.p2, edge.p1)] else: outer_edges = [edge] # calculate the maximum diagonal length within the model x_dim = abs(model.maxx - model.minx) y_dim = abs(model.maxy - model.miny) z_dim = abs(model.maxz - model.minz) max_length = sqrt(x_dim ** 2 + y_dim ** 2 + z_dim ** 2) result = [] for edge in outer_edges: direction = up_vector.cross(edge.dir).normalized() if direction is None: continue direction = direction.mul(max_length) edge_dir = edge.p2.sub(edge.p1) # TODO: Adapt the number of potential starting positions to the length # of the line. Don't use 0.0 and 1.0 - this could result in ambiguous # collisions with triangles sharing these vertices. for factor in (0.5, epsilon, 1.0 - epsilon, 0.25, 0.75): start = edge.p1.add(edge_dir.mul(factor)) # We need to use the triangle collision algorithm here - because we # need the point of collision in the triangle. collisions = get_free_paths_triangles([model], cutter, start, start.add(direction), return_triangles=True) for index, coll in enumerate(collisions): if (index % 2 == 0) and (not coll[1] is None) \ and (not coll[2] is None) \ and (coll[0].sub(start).dot(direction) > 0): cl, hit_t, cp = coll break else: log.debug("Failed to detect any collision: " \ + "%s / %s -> %s" % (edge, start, direction)) continue proj_cp = plane.get_point_projection(cp) # e.g. the Spherical Cutter often does not collide exactly above # the potential collision line. # TODO: maybe an "is cp inside of the triangle" check would be good? if (triangle is hit_t) or (edge.is_point_inside(proj_cp)): result.append((cl, edge)) # continue with the next outer_edge break # Don't check triangles again that are completely above the z level and # did not return any collisions. if (len(result) == 0) and (triangle.minz > z): # None indicates that the triangle needs no further evaluation return None return result
if __name__ == "__main__": import sys if len(sys.argv) > 1: import pycam.Importers.DXFImporter as importer model = importer.import_model(sys.argv[1]) else: model = pycam.Geometry.Model.ContourModel() # convert some points to a 2D model points = ((0.0, 0.0, 0.0), (0.5, 0.0, 0.0), (0.5, 0.5, 0.0), (0.0, 0.0, 0.0)) print("original points: ", points) before = None for p in points: if before: model.append(Line(before, p)) before = p if len(sys.argv) > 2: offset = float(sys.argv[2]) else: offset = 0.4 # scale model within a range of -1..1 maxdim = max(model.maxx - model.minx, model.maxy - model.miny) # stay well below sqrt(2)/2 in all directions scale_value = 1.4 / maxdim print("Scaling factor: %f" % scale_value) model.scale(scale_value) shift_x = -(model.minx + (model.maxx - model.minx) / 2.0) shift_y = -(model.miny + (model.maxy - model.miny) / 2.0) print("Shifting x: ", shift_x) print("Shifting y: ", shift_y)
def intersect_circle_line(center, axis, radius, radiussq, direction, edge): # make a plane by sliding the line along the direction (1) d = edge.dir if d.dot(axis) == 0: if direction.dot(axis) == 0: return (None, None, INFINITE) plane = Plane(center, axis) (p1, l) = plane.intersect_point(direction, edge.p1) (p2, l) = plane.intersect_point(direction, edge.p2) pc = Line(p1, p2).closest_point(center) d_sq = pc.sub(center).normsq if d_sq >= radiussq: return (None, None, INFINITE) a = sqrt(radiussq - d_sq) d1 = p1.sub(pc).dot(d) d2 = p2.sub(pc).dot(d) ccp = None cp = None if abs(d1) < a - epsilon: ccp = p1 cp = p1.sub(direction.mul(l)) elif abs(d2) < a - epsilon: ccp = p2 cp = p2.sub(direction.mul(l)) elif ((d1 < -a + epsilon) and (d2 > a - epsilon)) \ or ((d2 < -a + epsilon) and (d1 > a - epsilon)): ccp = pc cp = pc.sub(direction.mul(l)) return (ccp, cp, -l) n = d.cross(direction) if n.norm == 0: # no contact point, but should check here if circle *always* intersects # line... return (None, None, INFINITE) n = n.normalized() # take a plane through the base plane = Plane(center, axis) # intersect base with line (lp, l) = plane.intersect_point(d, edge.p1) if not lp: return (None, None, INFINITE) # intersection of 2 planes: lp + \lambda v v = axis.cross(n) if v.norm == 0: return (None, None, INFINITE) v = v.normalized() # take plane through intersection line and parallel to axis n2 = v.cross(axis) if n2.norm == 0: return (None, None, INFINITE) n2 = n2.normalized() # distance from center to this plane dist = n2.dot(center) - n2.dot(lp) distsq = dist * dist if distsq > radiussq - epsilon: return (None, None, INFINITE) # must be on circle dist2 = sqrt(radiussq - distsq) if d.dot(axis) < 0: dist2 = -dist2 ccp = center.sub(n2.mul(dist)).sub(v.mul(dist2)) plane = Plane(edge.p1, d.cross(direction).cross(d)) (cp, l) = plane.intersect_point(direction, ccp) return (ccp, cp, l)