def _point_along_bez4(p0: Point23List, p1: Point23List, p2: Point23List, p3: Point23List, u: float) -> Point2: p0 = euclidify(p0) p1 = euclidify(p1) p2 = euclidify(p2) p3 = euclidify(p3) x = _bez03(u) * p0.x + _bez13(u) * p1.x + _bez23(u) * p2.x + _bez33( u) * p3.x y = _bez03(u) * p0.y + _bez13(u) * p1.y + _bez23(u) * p2.y + _bez33( u) * p3.y return Point2(x, y)
def _catmull_rom_segment(controls: FourPoints, subdivisions: int, include_last=False) -> List[Point23]: """ Returns `subdivisions` Points between the 2nd & 3rd elements of `controls`, on a quadratic curve that passes through all 4 control points. If `include_last` is True, return `subdivisions` + 1 points, the last being controls[2]. No reason to call this unless you're trying to do something very specific """ pos: Point23 = None positions: List[Point23] = [] num_points = subdivisions if include_last: num_points += 1 p0, p1, p2, p3 = [euclidify(p, Point2) for p in controls] a = 2 * p1 b = p2 - p0 c = 2 * p0 - 5 * p1 + 4 * p2 - p3 d = -p0 + 3 * p1 - 3 * p2 + p3 for i in range(num_points): t = i / subdivisions pos = 0.5 * (a + (b * t) + (c * t * t) + (d * t * t * t)) positions.append(Point2(*pos)) return positions
def test_euclidify_non_mutating(self): base_tri = [Point2(0, 0), Point2(10, 0), Point2(0, 10)] next_tri = euclidify(base_tri, Point2) expected = 3 actual = len(base_tri) self.assertEqual(expected, actual, 'euclidify should not mutate its arguments')
def test_fillet_2d_add(self): pts = [[0, 5], [5, 5], [5, 0], [10, 0], [10, 10], [0, 10], ] p = polygon(pts) three_points = [euclidify(pts[0:3], Point2)] actual = fillet_2d(three_points, orig_poly=p, fillet_rad=2, remove_material=False) expected = 'union(){polygon(points=[[0,5],[5,5],[5,0],[10,0],[10,10],[0,10]]);translate(v=[3.0000000000,3.0000000000]){difference(){intersection(){rotate(a=359.9000000000){translate(v=[-998,0,0]){square(center=false,size=[1000,1000]);}}rotate(a=450.1000000000){translate(v=[-998,-1000,0]){square(center=false,size=[1000,1000]);}}}circle(r=2);}}}' self.assertEqualOpenScadObject(expected, actual)
def catmull_rom_points(points: Sequence[Point23List], subdivisions: int = 10, close_loop: bool = False, start_tangent: Vec23 = None, end_tangent: Vec23 = None) -> List[Point23]: """ Return a smooth set of points through `points`, with `subdivision` points between each pair of control points. If `close_loop` is False, `start_tangent` and `end_tangent` can specify tangents at the open ends of the returned curve. If not supplied, tangents will be colinear with first and last supplied segments Credit due: Largely taken from C# code at: https://www.habrador.com/tutorials/interpolation/1-catmull-rom-splines/ retrieved 20190712 """ catmull_points: List[Point23] = [] cat_points: List[Point23] = [] # points_list = cast(List[Point23], points) points_list = list([euclidify(p, Point2) for p in points]) if close_loop: cat_points = [points_list[-1]] + points_list + [points_list[0]] else: # Use supplied tangents or just continue the ends of the supplied points start_tangent = start_tangent or (points_list[1] - points_list[0]) end_tangent = end_tangent or (points_list[-2] - points_list[-1]) cat_points = [points_list[0] + start_tangent ] + points_list + [points_list[-1] + end_tangent] last_point_range = len(cat_points) - 2 if close_loop else len( cat_points) - 3 for i in range(0, last_point_range): include_last = True if i == last_point_range - 1 else False controls = cat_points[i:i + 4] # If we're closing a loop, controls needs to wrap around the end of the array points_needed = 4 - len(controls) if points_needed > 0: controls += cat_points[0:points_needed] controls_tuple = cast(FourPoints, controls) catmull_points += _catmull_rom_segment(controls_tuple, subdivisions, include_last) return catmull_points
def test_fillet_2d_add(self): pts = [ [0, 5], [5, 5], [5, 0], [10, 0], [10, 10], [0, 10], ] p = polygon(pts) newp = fillet_2d(euclidify(pts[0:3], Point3), orig_poly=p, fillet_rad=2, remove_material=False) expected = '\n\nunion() {\n\tpolygon(paths = [[0, 1, 2, 3, 4, 5]], points = [[0, 5], [5, 5], [5, 0], [10, 0], [10, 10], [0, 10]]);\n\ttranslate(v = [3.0000000000, 3.0000000000, 0.0000000000]) {\n\t\tdifference() {\n\t\t\tintersection() {\n\t\t\t\trotate(a = 358.0000000000) {\n\t\t\t\t\ttranslate(v = [-998, 0, 0]) {\n\t\t\t\t\t\tsquare(center = false, size = [1000, 1000]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\trotate(a = 452.0000000000) {\n\t\t\t\t\ttranslate(v = [-998, -1000, 0]) {\n\t\t\t\t\t\tsquare(center = false, size = [1000, 1000]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcircle(r = 2);\n\t\t}\n\t}\n}' actual = scad_render(newp) self.assertEqual(expected, actual)
def test_catmull_rom_prism(self): sides = 3 UP = Vector3(0, 0, 1) control_points = [[10, 10, 0], [10, 10, 5], [8, 8, 15]] cat_tube = [] angle_step = 2 * pi / sides for i in range(sides): rotated_controls = list( (euclidify(p, Point3).rotate_around(UP, angle_step * i) for p in control_points)) cat_tube.append(rotated_controls) poly = catmull_rom_prism(cat_tube, self.subdivisions, closed_ring=True, add_caps=True) actual = (len(poly.params['points']), len(poly.params['faces'])) expected = (37, 62) self.assertEqual(expected, actual)
def extrude_along_path( shape_pts:Points, path_pts:Points, scales:Sequence[Union[Vector2, float, Tuple2]] = None, rotations: Sequence[float] = None, transforms: Sequence[Point3Transform] = None, connect_ends = False, cap_ends = True) -> OpenSCADObject: ''' Extrude the curve defined by shape_pts along path_pts. -- For predictable results, shape_pts must be planar, convex, and lie in the XY plane centered around the origin. *Some* nonconvexity (e.g, star shapes) and nonplanarity will generally work fine -- len(scales) should equal len(path_pts). No-op if not supplied Each entry may be a single number for uniform scaling, or a pair of numbers (or Point2) for differential X/Y scaling If not supplied, no scaling will occur. -- len(rotations) should equal 1 or len(path_pts). No-op if not supplied. Each point in shape_pts will be rotated by rotations[i] degrees at each point in path_pts. Or, if only one rotation is supplied, the shape will be rotated smoothly over rotations[0] degrees in the course of the extrusion -- len(transforms) should be 1 or be equal to len(path_pts). No-op if not supplied. Each entry should be have the signature: def transform_func(p:Point3, path_norm:float, loop_norm:float): Point3 where path_norm is in [0,1] and expresses progress through the extrusion and loop_norm is in [0,1] and express progress through a single loop of the extrusion -- if connect_ends is True, the first and last loops of the extrusion will be joined, which is useful for toroidal geometries. Overrides cap_ends -- if cap_ends is True, each point in the first and last loops of the extrusion will be connected to the centroid of that loop. For planar, convex shapes, this works nicely. If shape is less planar or convex, some self-intersection may happen. Not applied if connect_ends is True ''' polyhedron_pts:Points= [] facet_indices:List[Tuple[int, int, int]] = [] # Make sure we've got Euclid Point3's for all elements shape_pts = euclidify(shape_pts, Point3) path_pts = euclidify(path_pts, Point3) src_up = Vector3(0, 0, 1) shape_pt_count = len(shape_pts) tangent_path_points: List[Point3] = [] if connect_ends: tangent_path_points = [path_pts[-1]] + path_pts + [path_pts[0]] else: first = Point3(*(path_pts[0] - (path_pts[1] - path_pts[0]))) last = Point3(*(path_pts[-1] - (path_pts[-2] - path_pts[-1]))) tangent_path_points = [first] + path_pts + [last] tangents = [tangent_path_points[i+2] - tangent_path_points[i] for i in range(len(path_pts))] for which_loop in range(len(path_pts)): # path_normal is 0 at the first path_pts and 1 at the last path_normal = which_loop/ (len(path_pts) - 1) path_pt = path_pts[which_loop] tangent = tangents[which_loop] scale = scales[which_loop] if scales else 1 rotate_degrees = None if rotations: rotate_degrees = rotations[which_loop] if len(rotations) > 1 else rotations[0] * path_normal transform_func = None if transforms: transform_func = transforms[which_loop] if len(transforms) > 1 else transforms[0] this_loop = shape_pts[:] this_loop = _scale_loop(this_loop, scale) this_loop = _rotate_loop(this_loop, rotate_degrees) this_loop = _transform_loop(this_loop, transform_func, path_normal) this_loop = transform_to_point(this_loop, dest_point=path_pt, dest_normal=tangent, src_up=src_up) loop_start_index = which_loop * shape_pt_count if (which_loop < len(path_pts) - 1): loop_facets = _loop_facet_indices(loop_start_index, shape_pt_count) facet_indices += loop_facets # Add the transformed points & facets to our final list polyhedron_pts += this_loop if connect_ends: next_loop_start_index = len(polyhedron_pts) - shape_pt_count loop_facets = _loop_facet_indices(0, shape_pt_count, next_loop_start_index) facet_indices += loop_facets elif cap_ends: # endcaps at start & end of extrusion # NOTE: this block adds points & indices to the polyhedron, so it's # very sensitive to the order this is happening in start_cap_index = len(polyhedron_pts) end_cap_index = start_cap_index + 1 last_loop_start_index = len(polyhedron_pts) - shape_pt_count start_loop_pts = polyhedron_pts[:shape_pt_count] end_loop_pts = polyhedron_pts[last_loop_start_index:] start_loop_indices = list(range(0, shape_pt_count)) end_loop_indices = list(range(last_loop_start_index, last_loop_start_index + shape_pt_count)) start_centroid, start_facet_indices = _end_cap(start_cap_index, start_loop_pts, start_loop_indices) end_centroid, end_facet_indices = _end_cap(end_cap_index, end_loop_pts, end_loop_indices) polyhedron_pts += [start_centroid, end_centroid] facet_indices += start_facet_indices facet_indices += end_facet_indices return polyhedron(points=euc_to_arr(polyhedron_pts), faces=facet_indices) # type: ignore