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 intersect_cylinder_line(self, direction, edge, start=None): if start is None: start = self.location (ccp, cp, l) = intersect_cylinder_line( padd(psub(start, self.location), self.center), self.axis, self.distance_radius, self.distance_radiussq, direction, edge) # offset intersection if ccp: cl = padd(start, psub(cp, ccp)) return (cl, ccp, cp, l) return (None, None, None, INFINITE)
def intersect_sphere_point(self, direction, point, start=None): if start is None: start = self.location (ccp, cp, l) = intersect_sphere_point(padd(psub(start, self.location), self.center), self.distance_radius, self.distance_radiussq, direction, point) # offset intersection cl = None if cp: cl = padd(start, pmul(direction, l)) return (cl, ccp, cp, l)
def intersect_sphere_plane(self, direction, triangle, start=None): if start is None: start = self.location (ccp, cp, d) = intersect_sphere_plane( padd(psub(start, self.location), self.center), self.distance_radius, direction, triangle) # offset intersection if ccp: cl = padd(cp, psub(start, ccp)) return (cl, ccp, cp, d) return (None, None, None, INFINITE)
def intersect_torus_plane(self, direction, triangle, start=None): if start is None: start = self.location (ccp, cp, l) = intersect_torus_plane( padd(psub(start, self.location), self.center), self.axis, self.distance_majorradius, self.distance_minorradius, direction, triangle) if cp: cl = padd(cp, psub(start, ccp)) return (cl, ccp, cp, l) return (None, None, None, INFINITE)
def intersect_torus_point(self, direction, point, start=None): if start is None: start = self.location (ccp, cp, l) = intersect_torus_point( padd(psub(start, self.location), self.center), self.axis, self.distance_majorradius, self.distance_minorradius, self.distance_majorradiussq, self.distance_minorradiussq, direction, point) if ccp: cl = padd(point, psub(start, ccp)) return (cl, ccp, point, l) return (None, None, None, INFINITE)
def subdivide(self, depth): sub = [] if depth == 0: sub.append(self) else: p4 = pdiv(padd(self.p1, self.p2), 2) p5 = pdiv(padd(self.p2, self.p3), 2) p6 = pdiv(padd(self.p3, self.p1), 2) sub += Triangle(self.p1, p4, p6).subdivide(depth - 1) sub += Triangle(p6, p5, self.p3).subdivide(depth - 1) sub += Triangle(p6, p4, p5).subdivide(depth - 1) sub += Triangle(p4, self.p2, p5).subdivide(depth - 1) return sub
def intersect_cylinder_point(center, axis, radius, radiussq, direction, point): # take a plane along direction and axis n = pnormalized(pcross(direction, axis)) # distance of the point to this plane d = pdot(n, point) - pdot(n, center) if abs(d) > radius - epsilon: return (None, None, INFINITE) # ccl is on cylinder d2 = sqrt(radiussq - d * d) ccl = padd(padd(center, pmul(n, d)), pmul(direction, d2)) # take plane through ccl and axis plane = Plane(ccl, direction) # intersect point with plane (ccp, l) = plane.intersect_point(direction, point) return (ccp, point, -l)
def filter_toolpath(self, toolpath): feedrate = min_feedrate = 1 new_path = [] last_pos = None limit = self.settings["timelimit"] duration = 0 for move_type, args in toolpath: if move_type in (MOVE_STRAIGHT, MOVE_STRAIGHT_RAPID): if last_pos: new_distance = pdist(args, last_pos) new_duration = new_distance / max(feedrate, min_feedrate) if (new_duration > 0) and (duration + new_duration > limit): partial = (limit - duration) / new_duration destination = padd(last_pos, pmul(psub(args, last_pos), partial)) duration = limit else: destination = args duration += new_duration else: destination = args new_path.append((move_type, destination)) last_pos = args if (move_type == MACHINE_SETTING) and (args[0] == "feedrate"): feedrate = args[1] if duration >= limit: break return new_path
def calc_normal(main, normals): suitable = (0, 0, 0, 'v') for normal, weight in normals: dot = pdot(main, normal) if dot > 0: suitable = padd(suitable, pmul(normal, weight * dot)) return pnormalized(suitable)
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_middle_of_line(self, index): if (index >= len(self._points)) \ or (not self.is_closed and index == len(self._points) - 1): return None else: return pdiv(padd(self._points[index], self._points[(index + 1) % len(self._points)]), 2)
def filter_toolpath(self, toolpath): feedrate = min_feedrate = 1 new_path = [] last_pos = None limit = self.settings["timelimit"] duration = 0 for move_type, args in toolpath: if move_type in (MOVE_STRAIGHT, MOVE_STRAIGHT_RAPID): if last_pos: new_distance = pdist(args, last_pos) new_duration = new_distance / max(feedrate, min_feedrate) if (new_duration > 0) and (duration + new_duration > limit): partial = (limit - duration) / new_duration destination = padd(last_pos, pmul(psub(args, last_pos), partial)) duration = limit else: destination = args duration += new_duration else: destination = args new_path.append((move_type, destination)) last_pos = args if (move_type == MACHINE_SETTING) and (args[0] == "feedrate"): feedrate = args[1] if duration >= limit: break return new_path
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 filter_toolpath(self, toolpath): feedrate = min_feedrate = 1 new_path = [] last_pos = None limit = self.settings["timelimit"] duration = 0 for step in toolpath: if step.action in MOVES_LIST: if last_pos: new_distance = pdist(step.position, last_pos) new_duration = new_distance / max(feedrate, min_feedrate) if (new_duration > 0) and (duration + new_duration > limit): partial = (limit - duration) / new_duration destination = padd(last_pos, pmul(psub(step.position, last_pos), partial)) duration = limit else: destination = step.position duration += new_duration else: destination = step.position new_path.append(ToolpathSteps.get_step_class_by_action(step.action)(destination)) last_pos = step.position if (step.action == MACHINE_SETTING) and (step.key == "feedrate"): feedrate = step.value if duration >= limit: break return new_path
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 = psub(cutter_location, wl_proj.closest_point(cutter_location)) # increase the shift width slightly to avoid "touch" collisions shift = pmul(shift, 1.0 + epsilon) shifted_waterline = Line(padd(wl_proj.p1, shift), padd(wl_proj.p2, shift)) return shifted_waterline
def intersect_cylinder_vertex(self, direction, point, start=None): if start is None: start = self.location (cl, ccp, cp, l) = self.intersect_cylinder_point(direction, point, start=start) if ccp and ccp[2] < padd(psub(start, self.location), self.center)[2]: return (None, INFINITE, None) return (cl, l, cp)
def intersect_torus_plane(center, axis, majorradius, minorradius, direction, triangle): # take normal to the plane n = triangle.normal if pdot(n, direction) == 0: return (None, None, INFINITE) if pdot(n, axis) == 1: return (None, None, INFINITE) # find place on torus where surface normal is n b = pmul(n, -1) z = axis a = psub(b, pmul(z, pdot(z, b))) a_sq = pnormsq(a) if a_sq <= 0: return (None, None, INFINITE) a = pdiv(a, sqrt(a_sq)) ccp = padd(padd(center, pmul(a, majorradius)), pmul(b, minorradius)) # find intersection with plane (cp, l) = triangle.plane.intersect_point(direction, ccp) return (ccp, cp, l)
def get_shifted_vertex(self, index, offset): p1 = self._points[index] p2 = self._points[(index + 1) % len(self._points)] cross_offset = pnormalized(pcross(psub(p2, p1), self.plane.n)) bisector_normalized = self.get_bisector(index) factor = pdot(cross_offset, bisector_normalized) if factor != 0: bisector_sized = pmul(bisector_normalized, offset / factor) return padd(p1, bisector_sized) else: return p2
def get_intersection(self, line, infinite_lines=False): """ Get the point of intersection between two lines. Intersections outside the length of these lines are ignored. Returns (None, None) if no valid intersection was found. Otherwise the result is (CollisionPoint, distance). Distance is between 0 and 1. """ x1, x2, x3, x4 = self.p1, self.p2, line.p1, line.p2 a = psub(x2, x1) b = psub(x4, x3) c = psub(x3, x1) # see http://mathworld.wolfram.com/Line-LineIntersection.html (24) try: factor = pdot(pcross(c, b), pcross(a, b)) / pnormsq(pcross(a, b)) except ZeroDivisionError: # lines are parallel # check if they are _one_ line if pnorm(pcross(a, c)) != 0: # the lines are parallel with a distance return None, None # the lines are on one straight candidates = [] if self.is_point_inside(x3): candidates.append((x3, pnorm(c) / pnorm(a))) elif self.is_point_inside(x4): candidates.append((x4, pdist(line.p2, self.p1) / pnorm(a))) elif line.is_point_inside(x1): candidates.append((x1, 0)) elif line.is_point_inside(x2): candidates.append((x2, 1)) else: return None, None # return the collision candidate with the lowest distance candidates.sort(key=lambda collision: collision[1]) return candidates[0] if infinite_lines or (-epsilon <= factor <= 1 + epsilon): intersec = padd(x1, pmul(a, factor)) # check if the intersection is between x3 and x4 if infinite_lines: return intersec, factor elif ((min(x3[0], x4[0]) - epsilon <= intersec[0] <= max(x3[0], x4[0]) + epsilon) and (min(x3[1], x4[1]) - epsilon <= intersec[1] <= max(x3[1], x4[1]) + epsilon) and (min(x3[2], x4[2]) - epsilon <= intersec[2] <= max(x3[2], x4[2]) + epsilon)): return intersec, factor else: # intersection outside of the length of line(x3, x4) return None, None else: # intersection outside of the length of line(x1, x2) return None, None
def intersect_point(self, direction, point): if (direction is not None) and (pnorm(direction) != 1): # calculations will go wrong, if the direction is not a unit vector direction = pnormalized(direction) if direction is None: return (None, INFINITE) denom = pdot(self.n, direction) if denom == 0: return (None, INFINITE) l = -(pdot(self.n, point) - pdot(self.n, self.p)) / denom cp = padd(point, pmul(direction, l)) return (cp, l)
def intersect_sphere_plane(center, radius, direction, triangle): # let n be the normal to the plane n = triangle.normal if pdot(n, direction) == 0: return (None, None, INFINITE) # the cutter contact point is on the sphere, where the surface normal is n if pdot(n, direction) < 0: ccp = psub(center, pmul(n, radius)) else: ccp = padd(center, pmul(n, radius)) # intersect the plane with a line through the contact point (cp, d) = triangle.plane.intersect_point(direction, ccp) return (ccp, cp, d)
def intersect_cylinder_edge(self, direction, edge, start=None): if start is None: start = self.location (cl, ccp, cp, l) = self.intersect_cylinder_line(direction, edge, start=start) if not ccp: return (None, INFINITE, None) m = pdot(psub(cp, edge.p1), edge.dir) if (m < -epsilon) or (m > edge.len + epsilon): return (None, INFINITE, None) if ccp[2] < padd(psub(start, self.location), self.center)[2]: return (None, INFINITE, None) return (cl, l, cp)
def draw_direction_cone_mesh(self, p1, p2, position=0.5, precision=12, size=0.1): distance = psub(p2, p1) length = pnorm(distance) direction = pnormalized(distance) if direction is None or length < 0.5: # zero-length line return [] cone_length = length * size cone_radius = cone_length / 3.0 bottom = padd(p1, pmul(psub(p2, p1), position - size / 2)) top = padd(p1, pmul(psub(p2, p1), position + size / 2)) # generate a a line perpendicular to this line, cross product is good at this cross = pcross(direction, (0, 0, -1)) conepoints = [] if pnorm(cross) != 0: # The line direction is not in line with the z axis. conep1 = padd(bottom, pmul(cross, cone_radius)) conepoints = [ self._rotate_point(conep1, bottom, direction, x) for x in numpy.linspace(0, 2 * math.pi, precision) ] else: # Z axis # just add cone radius to the x axis and rotate the point conep1 = (bottom[0] + cone_radius, bottom[1], bottom[2]) conepoints = [ self._rotate_point(conep1, p1, direction, x) for x in numpy.linspace(0, 2 * math.pi, precision) ] triangles = [(top, conepoints[idx], conepoints[idx + 1]) for idx in range(len(conepoints) - 1)] return triangles
def intersect_cylinder_line(center, axis, radius, radiussq, direction, edge): d = edge.dir # take a plane throught the line and along the cylinder axis (1) n = pcross(d, axis) if pnorm(n) == 0: # no contact point, but should check here if cylinder *always* # intersects line... return (None, None, INFINITE) n = pnormalized(n) # the contact line between the cylinder and this plane (1) # is where the surface normal is perpendicular to the plane # so line := ccl + \lambda * axis if pdot(n, direction) < 0: ccl = psub(center, pmul(n, radius)) else: ccl = padd(center, pmul(n, radius)) # now extrude the contact line along the direction, this is a plane (2) n2 = pcross(direction, axis) if pnorm(n2) == 0: # no contact point, but should check here if cylinder *always* # intersects line... return (None, None, INFINITE) n2 = pnormalized(n2) plane1 = Plane(ccl, n2) # intersect this plane with the line, this gives us the contact point (cp, l) = plane1.intersect_point(d, edge.p1) if not cp: return (None, None, INFINITE) # now take a plane through the contact line and perpendicular to the # direction (3) plane2 = Plane(ccl, direction) # the intersection of this plane (3) with the line through the contact point # gives us the cutter contact point (ccp, l) = plane2.intersect_point(direction, cp) cp = padd(ccp, pmul(direction, -l)) return (ccp, cp, -l)
def get_bisector(p1, p2, p3, up_vector): """ Calculate the bisector between p1, p2 and p3, whereas p2 is the origin of the angle. """ d1 = pnormalized(psub(p2, p1)) d2 = pnormalized(psub(p2, p3)) bisector_dir = pnormalized(padd(d1, d2)) if bisector_dir is None: # the two vectors pointed to opposite directions bisector_dir = pnormalized(pcross(d1, up_vector)) else: skel_up_vector = pcross(bisector_dir, psub(p2, p1)) if pdot(up_vector, skel_up_vector) < 0: # reverse the skeleton vector to point outwards bisector_dir = pmul(bisector_dir, -1) return bisector_dir
def intersect_circle_plane(center, radius, direction, triangle): # let n be the normal to the plane n = triangle.normal if pdot(n, direction) == 0: return (None, None, INFINITE) # project onto z=0 n2 = (n[0], n[1], 0) if pnorm(n2) == 0: (cp, d) = triangle.plane.intersect_point(direction, center) ccp = psub(cp, pmul(direction, d)) return (ccp, cp, d) n2 = pnormalized(n2) # the cutter contact point is on the circle, where the surface normal is n ccp = padd(center, pmul(n2, -radius)) # intersect the plane with a line through the contact point (cp, d) = triangle.plane.intersect_point(direction, ccp) return (ccp, cp, d)
def _add_cuboid_to_model(model, start, direction, height, width): up = pmul((0, 0, 1, 'v'), height) ortho_dir = pnormalized(pcross(direction, up)) start1 = padd(start, pmul(ortho_dir, -width / 2)) start2 = padd(start1, up) start3 = padd(start2, pmul(ortho_dir, width)) start4 = psub(start3, up) end1 = padd(start1, direction) end2 = padd(start2, direction) end3 = padd(start3, direction) end4 = padd(start4, direction) faces = ((start1, start2, start3, start4), (start1, end1, end2, start2), (start2, end2, end3, start3), (start3, end3, end4, start4), (start4, end4, end1, start1), (end4, end3, end2, end1)) for face in faces: t1, t2 = _get_triangles_for_face(face) model.append(t1) model.append(t2)
def intersect_sphere_point(center, radius, radiussq, direction, point): # line equation # (1) x = p_0 + \lambda * d # sphere equation # (2) (x-x_0)^2 = R^2 # (1) in (2) gives a quadratic in \lambda p0_x0 = psub(center, point) a = pnormsq(direction) b = 2 * pdot(p0_x0, direction) c = pnormsq(p0_x0) - radiussq d = b * b - 4 * a * c if d < 0: return (None, None, INFINITE) if a < 0: l = (-b + sqrt(d)) / (2 * a) else: l = (-b - sqrt(d)) / (2 * a) # cutter contact point ccp = padd(point, pmul(direction, -l)) return (ccp, point, l)
def get_spiral_layer(minx, maxx, miny, maxy, z, line_distance, step_width, grid_direction, start_position, rounded_corners, reverse): current_location = _get_position(minx, maxx, miny, maxy, z, start_position) if line_distance > 0: line_steps_x = math.ceil((float(maxx - minx) / line_distance)) line_steps_y = math.ceil((float(maxy - miny) / line_distance)) line_distance_x = (maxx - minx) / line_steps_x line_distance_y = (maxy - miny) / line_steps_y lines = get_spiral_layer_lines(minx, maxx, miny, maxy, z, line_distance_x, line_distance_y, grid_direction, start_position, current_location) if reverse: lines.reverse() # turn the lines into steps if rounded_corners: rounded_lines = [] previous = None for index, (start, end) in enumerate(lines): radius = 0.5 * min(line_distance_x, line_distance_y) edge_vector = psub(end, start) # TODO: ellipse would be better than arc offset = pmul(pnormalized(edge_vector), radius) if previous: start = padd(start, offset) center = padd(previous, offset) up_vector = pnormalized( pcross(psub(previous, center), psub(start, center))) north = padd(center, (1.0, 0.0, 0.0, 'v')) angle_start = get_angle_pi( north, center, previous, up_vector, pi_factor=True) * 180.0 angle_end = get_angle_pi( north, center, start, up_vector, pi_factor=True) * 180.0 # TODO: remove these exceptions based on up_vector.z (get_points_of_arc does # not respect the plane, yet) if up_vector[2] < 0: angle_start, angle_end = -angle_end, -angle_start arc_points = get_points_of_arc(center, radius, angle_start, angle_end) if up_vector[2] < 0: arc_points.reverse() for arc_index in range(len(arc_points) - 1): p1_coord = arc_points[arc_index] p2_coord = arc_points[arc_index + 1] p1 = (p1_coord[0], p1_coord[1], z) p2 = (p2_coord[0], p2_coord[1], z) rounded_lines.append((p1, p2)) if index != len(lines) - 1: end = psub(end, offset) previous = end rounded_lines.append((start, end)) lines = rounded_lines for start, end in lines: points = [] if step_width is None: points.append(start) points.append(end) else: line = Line(start, end) 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) if reverse: points.reverse() yield points
def get_bezier_lines(points_with_bulge, segments=32): # TODO: add a recursive algorithm for more than two points if len(points_with_bulge) != 2: return [] else: result_points = [] p1, bulge1 = points_with_bulge[0] p2, bulge2 = points_with_bulge[1] if not bulge1 and not bulge2: # straight line return [Line(p1, p2)] straight_dir = pnormalized(psub(p2, p1)) bulge1 = math.atan(bulge1) rot_matrix = Matrix.get_rotation_matrix_axis_angle((0, 0, 1), -2 * bulge1, use_radians=True) dir1_mat = Matrix.multiply_vector_matrix( (straight_dir[0], straight_dir[1], straight_dir[2]), rot_matrix) dir1 = (dir1_mat[0], dir1_mat[1], dir1_mat[2], 'v') if bulge2 is None: bulge2 = bulge1 else: bulge2 = math.atan(bulge2) rot_matrix = Matrix.get_rotation_matrix_axis_angle((0, 0, 1), 2 * bulge2, use_radians=True) dir2_mat = Matrix.multiply_vector_matrix( (straight_dir[0], straight_dir[1], straight_dir[2]), rot_matrix) dir2 = (dir2_mat[0], dir2_mat[1], dir2_mat[2], 'v') # interpretation of bulge1 and bulge2: # /// taken from http://paulbourke.net/dataformats/dxf/dxf10.html /// # The bulge is the tangent of 1/4 the included angle for an arc # segment, made negative if the arc goes clockwise from the start # point to the end point; a bulge of 0 indicates a straight segment, # and a bulge of 1 is a semicircle. alpha = 2 * (abs(bulge1) + abs(bulge2)) dist = pdist(p2, p1) # calculate the radius of the circumcircle - avoiding divide-by-zero if (abs(alpha) < epsilon) or (abs(math.pi - alpha) < epsilon): radius = dist / 2.0 else: # see http://en.wikipedia.org/wiki/Law_of_sines radius = abs(dist / math.sin(alpha / 2.0)) / 2.0 # The calculation of "factor" is based on random guessing - but it # seems to work well. factor = 4 * radius * math.tan(alpha / 4.0) dir1 = pmul(dir1, factor) dir2 = pmul(dir2, factor) for index in range(segments + 1): # t: 0..1 t = float(index) / segments # see: http://en.wikipedia.org/wiki/Cubic_Hermite_spline p = padd( pmul(p1, 2 * t**3 - 3 * t**2 + 1), padd( pmul(dir1, t**3 - 2 * t**2 + t), padd(pmul(p2, -2 * t**3 + 3 * t**2), pmul(dir2, t**3 - t**2)))) result_points.append(p) # create lines result = [] for index in range(len(result_points) - 1): result.append(Line(result_points[index], result_points[index + 1])) return result