def transform_to_point( body: OpenSCADObject, dest_point: Point3, dest_normal: Vector3, src_point: Point3=Point3(0, 0, 0), src_normal: Vector3=Vector3(0, 1, 0), src_up: Vector3=Vector3(0, 0, 1)) -> OpenSCADObject: # Transform body to dest_point, looking at dest_normal. # Orientation & offset can be changed by supplying the src arguments # Body may be: # -- an openSCAD object # -- a list of 3-tuples or PyEuclid Point3s # -- a single 3-tuple or Point3 dest_point = euclidify(dest_point, Point3) dest_normal = euclidify(dest_normal, Vector3) at = dest_point + dest_normal EUC_UP = euclidify(UP_VEC) EUC_FORWARD = euclidify(FORWARD_VEC) EUC_ORIGIN = euclidify(ORIGIN, Vector3) # if dest_normal and src_up are parallel, the transform collapses # all points to dest_point. Instead, use EUC_FORWARD if needed if dest_normal.cross(src_up) == EUC_ORIGIN: if src_up.cross(EUC_UP) == EUC_ORIGIN: src_up = EUC_FORWARD else: src_up = EUC_UP def _orig_euclid_look_at(eye, at, up): ''' Taken from the original source of PyEuclid's Matrix4.new_look_at() prior to 1184a07d119a62fc40b2c6becdbeaf053a699047 (11 Jan 2015), as discussed here: https://github.com/ezag/pyeuclid/commit/1184a07d119a62fc40b2c6becdbeaf053a699047 We were dependent on the old behavior, which is duplicated here: ''' z = (eye - at).normalized() x = up.cross(z).normalized() y = z.cross(x) m = Matrix4.new_rotate_triple_axis(x, y, z) m.d, m.h, m.l = eye.x, eye.y, eye.z return m look_at_matrix = _orig_euclid_look_at(eye=dest_point, at=at, up=src_up) if is_scad(body): # If the body being altered is a SCAD object, do the matrix mult # in OpenSCAD sc_matrix = scad_matrix(look_at_matrix) res = multmatrix(m=sc_matrix)(body) else: body = euclidify(body, Point3) if isinstance(body, (list, tuple)): res = [look_at_matrix * p for p in body] else: res = look_at_matrix * body return res
def draw_coords(mtx, size, width=1): x = Vector3(size, 0, 0) y = Vector3(0, size, 0) z = Vector3(0, 0, size) orig = mtx[12:15] draw_line(orig, orig + mtx * x, (1, 0, 0), width) draw_line(orig, orig + mtx * y, (0, 1, 0), width) draw_line(orig, orig + mtx * z, (0, 0, 1), width)
def _calculate_sun_vector(self): """Calculate sun vector for this sun.""" z_axis = Vector3(0., 0., -1.) x_axis = Vector3(1., 0., 0.) north_vector = Vector3(0., 1., 0.) # rotate north vector based on azimuth, altitude, and north _sun_vector = north_vector \ .rotate_around(x_axis, self.altitude_in_radians) \ .rotate_around(z_axis, self.azimuth_in_radians) \ .rotate_around(z_axis, math.radians(-1 * self.north_angle)) _sun_vector.normalize() #.flip() self._sun_vector = _sun_vector
def on_draw(): a = 1.6 b = a * sqrt(3) / 3 r = sqrt(1 - b * b) R = Vector3(a, a, a).normalized() * b cs = Matrix4.new_translate(R[0], R[1], R[2]) * Matrix4.new_look_at( Vector3(0, 0, 0), R, Vector3(0, 1, 0)) glDisable(GL_DEPTH_TEST) glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) glLoadIdentity() draw_coords(Matrix4.new_identity(), 2) draw_line((0, 0, 0), R, (1, 1, 0)) draw_coords(cs, r, 3) glEnable(GL_DEPTH_TEST) draw_triangle((a, 0, 0), (0, a, 0), (0, 0, a), (.5, .5, 1), .2) draw_sphere(3, (1, .5, .5), .2)
def _parallel_seg(p, q, offset, normal=Vector3(0, 0, 1), direction=LEFT): # returns a PyEuclid Line3 parallel to pq, in the plane determined # by p,normal, to the left or right of pq. v = q - p angle = direction rot_v = v.rotate_around(axis=normal, theta=angle) rot_v.set_length(offset) return Line3(p + rot_v, v)
def _get_direction(self, rotation: Tuple[int, int, int]): """ Return a vector pointing in the positive direction of the current view. That means the direction in which the layer structure is stacked, i.e. "up". The "index" tells where along this axis the cursor is. """ # TODO This is pretty crude. Also, use numpy instead of euclid? rx, ry, rz = rotation v = Vector3(0, 0, 1) zaxis = Vector3(0, 0, 1) xaxis = Vector3(1, 0, 0) yaxis = Vector3(0, 1, 0) if ry: v = v.rotate_around(yaxis, -ry * math.pi/2) if rx: v = v.rotate_around(xaxis, -rx * math.pi/2) if rz: v = v.rotate_around(zaxis, -rz * math.pi/2) return tuple(int(a) for a in v)
def _calculate_sun_vector(self): """Calculate sun vector for this sun.""" z_axis = Vector3(0., 0., -1.) x_axis = Vector3(1., 0., 0.) north_vector = Vector3(0., 1., 0.) # rotate north vector based on azimuth, altitude, and north _sun_vector = north_vector \ .rotate_around(x_axis, self.altitude_in_radians) \ .rotate_around(z_axis, self.azimuth_in_radians) \ .rotate_around(z_axis, math.radians(-1 * self.north_angle)) _sun_vector.normalize() try: _sun_vector.flip() except AttributeError: # euclid3 _sun_vector = Vector3(-1 * _sun_vector.x, -1 * _sun_vector.y, -1 * _sun_vector.z) self._sun_vector = _sun_vector
def extrude_example_transforms() -> OpenSCADObject: path_rad = PATH_RAD height = 2 * SHAPE_RAD num_steps = 120 shape = circle_points(rad=path_rad, num_points=120) path = [Point3(0, 0, i) for i in frange(0, height, num_steps=num_steps)] max_rotation = radians(15) max_z_displacement = height / 10 up = Vector3(0, 0, 1) # The transforms argument is powerful. # Each point in the entire extrusion will call this function with unique arguments: # -- `path_norm` in [0, 1] specifying how far along in the extrusion a point's loop is # -- `loop_norm` in [0, 1] specifying where in its loop a point is. def point_trans(point: Point3, path_norm: float, loop_norm: float) -> Point3: # scale the point from 1x to 2x in the course of the # extrusion, scale = 1 + path_norm * path_norm / 2 p = scale * point # Rotate the points sinusoidally up to max_rotation p = p.rotate_around(up, max_rotation * sin(tau * path_norm)) # Oscillate z values sinusoidally, growing from # 0 magnitude to max_z_displacement max_z = lerp(path_norm, 0, 1, 0, max_z_displacement) angle = lerp(loop_norm, 0, 1, 0, 10 * tau) p.z += max_z * sin(angle) return p no_trans = make_label('No Transform') no_trans += down(height / 2)(extrude_along_path(shape, path, cap_ends=False)) # We can pass transforms a single function that will be called on all points, # or pass a list with a transform function for each point along path arb_trans = make_label('Arbitrary Transform') arb_trans += down(height / 2)(extrude_along_path(shape, path, transforms=[point_trans], cap_ends=False)) return no_trans + right(3 * path_rad)(arb_trans)
def test_custom_iterables(self): from euclid3 import Vector3 class CustomIterable: def __iter__(self): return iter([1, 2, 3]) expected = '\n\ncube(size = [1, 2, 3]);' iterables = [ [1, 2, 3], (1, 2, 3), Vector3(1, 2, 3), CustomIterable(), ] for iterable in iterables: name = type(iterable).__name__ actual = scad_render(cube(size=iterable)) self.assertEqual(expected, actual, f'{name} SolidPython not rendered correctly')
def centroid(points: Sequence[PointVec23]) -> PointVec23: if not points: raise ValueError(f"centroid(): argument `points` is empty") first = points[0] is_3d = isinstance(first, (Vector3, Point3)) if is_3d: total = Vector3(0, 0, 0) else: total = Vector2(0, 0) for p in points: total += p total /= len(points) if isinstance(first, Point2): return Point2(*total) elif isinstance(first, Point3): return Point3(*total) else: return total
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)
('right', right, [2], '\n\ntranslate(v = [2, 0, 0]);'), ('forward', forward, [2], '\n\ntranslate(v = [0, 2, 0]);'), ('back', back, [2], '\n\ntranslate(v = [0, -2, 0]);'), ('arc', arc, [10, 0, 90, 24], '\n\ndifference() {\n\tcircle($fn = 24, r = 10);\n\trotate(a = 0) {\n\t\ttranslate(v = [0, -10, 0]) {\n\t\t\tsquare(center = true, size = [30, 20]);\n\t\t}\n\t}\n\trotate(a = -90) {\n\t\ttranslate(v = [0, -10, 0]) {\n\t\t\tsquare(center = true, size = [30, 20]);\n\t\t}\n\t}\n}'), ('arc_inverted', arc_inverted, [10, 0, 90, 24], '\n\ndifference() {\n\tintersection() {\n\t\trotate(a = 0) {\n\t\t\ttranslate(v = [-990, 0, 0]) {\n\t\t\t\tsquare(center = false, size = [1000, 1000]);\n\t\t\t}\n\t\t}\n\t\trotate(a = 90) {\n\t\t\ttranslate(v = [-990, -1000, 0]) {\n\t\t\t\tsquare(center = false, size = [1000, 1000]);\n\t\t\t}\n\t\t}\n\t}\n\tcircle($fn = 24, r = 10);\n}'), ('transform_to_point_scad', transform_to_point, [cube(2), [2, 2, 2], [3, 3, 1]], '\n\nmultmatrix(m = [[0.7071067812, -0.1622214211, -0.6882472016, 2], [-0.7071067812, -0.1622214211, -0.6882472016, 2], [0.0000000000, 0.9733285268, -0.2294157339, 2], [0, 0, 0, 1.0000000000]]) {\n\tcube(size = 2);\n}'), ('extrude_along_path', extrude_along_path, [tri, [[0, 0, 0], [0, 20, 0]]], '\n\npolyhedron(faces = [[0, 3, 1], [1, 3, 4], [1, 4, 2], [2, 4, 5], [0, 2, 5], [0, 5, 3], [0, 1, 2], [3, 5, 4]], points = [[0.0000000000, 0.0000000000, 0.0000000000], [10.0000000000, 0.0000000000, 0.0000000000], [0.0000000000, 0.0000000000, 10.0000000000], [0.0000000000, 20.0000000000, 0.0000000000], [10.0000000000, 20.0000000000, 0.0000000000], [0.0000000000, 20.0000000000, 10.0000000000]]);'), ('extrude_along_path_vertical', extrude_along_path, [tri, [[0, 0, 0], [0, 0, 20]]], '\n\npolyhedron(faces = [[0, 3, 1], [1, 3, 4], [1, 4, 2], [2, 4, 5], [0, 2, 5], [0, 5, 3], [0, 1, 2], [3, 5, 4]], points = [[0.0000000000, 0.0000000000, 0.0000000000], [-10.0000000000, 0.0000000000, 0.0000000000], [0.0000000000, 10.0000000000, 0.0000000000], [0.0000000000, 0.0000000000, 20.0000000000], [-10.0000000000, 0.0000000000, 20.0000000000], [0.0000000000, 10.0000000000, 20.0000000000]]);'), ] other_test_cases = [ # Test name, function, args, expected value ('euclidify', euclidify, [[0, 0, 0]], 'Vector3(0.00, 0.00, 0.00)'), ('euclidify_recursive', euclidify, [[[0, 0, 0], [1, 0, 0]]], '[Vector3(0.00, 0.00, 0.00), Vector3(1.00, 0.00, 0.00)]'), ('euclidify_Vector', euclidify, [Vector3(0, 0, 0)], 'Vector3(0.00, 0.00, 0.00)'), ('euclidify_recursive_Vector', euclidify, [[Vector3(0, 0, 0), Vector3(0, 0, 1)]], '[Vector3(0.00, 0.00, 0.00), Vector3(0.00, 0.00, 1.00)]'), ('euclidify_3_to_2', euclidify, [Point3(0,1,2), Point2], 'Point2(0.00, 1.00)'), ('euc_to_arr', euc_to_arr, [Vector3(0, 0, 0)], '[0, 0, 0]'), ('euc_to_arr_recursive', euc_to_arr, [[Vector3(0, 0, 0), Vector3(0, 0, 1)]], '[[0, 0, 0], [0, 0, 1]]'), ('euc_to_arr_arr', euc_to_arr, [[0, 0, 0]], '[0, 0, 0]'), ('euc_to_arr_arr_recursive', euc_to_arr, [[[0, 0, 0], [1, 0, 0]]], '[[0, 0, 0], [1, 0, 0]]'), ('is_scad', is_scad, [cube(2)], 'True'), ('is_scad_false', is_scad, [2], 'False'), ('transform_to_point_single_arr', transform_to_point, [[1, 0, 0], [2, 2, 2], [3, 3, 1]], 'Point3(2.71, 1.29, 2.00)'), ('transform_to_point_single_pt3', transform_to_point, [Point3(1, 0, 0), [2, 2, 2], [3, 3, 1]], 'Point3(2.71, 1.29, 2.00)'), ('transform_to_point_arr_arr', transform_to_point, [[[1, 0, 0], [0, 1, 0], [0, 0, 1]], [2, 2, 2], [3, 3, 1]], '[Point3(2.71, 1.29, 2.00), Point3(1.84, 1.84, 2.97), Point3(1.31, 1.31, 1.77)]'), ('transform_to_point_pt3_arr', transform_to_point, [[Point3(1, 0, 0), Point3(0, 1, 0), Point3(0, 0, 1)], [2, 2, 2], [3, 3, 1]], '[Point3(2.71, 1.29, 2.00), Point3(1.84, 1.84, 2.97), Point3(1.31, 1.31, 1.77)]'), ('transform_to_point_redundant', transform_to_point, [[Point3(0, 0, 0), Point3(10, 0, 0), Point3(0, 10, 0)], [2, 2, 2], Vector3(0, 0, 1), Point3(0, 0, 0), Vector3(0, 1, 0), Vector3(0, 0, 1)], '[Point3(2.00, 2.00, 2.00), Point3(-8.00, 2.00, 2.00), Point3(2.00, 12.00, 2.00)]'), ('offset_points_inside', offset_points, [tri, 2, True], '[Point2(2.00, 2.00), Point2(5.17, 2.00), Point2(2.00, 5.17)]'), ('offset_points_outside', offset_points, [tri, 2, False], '[Point2(-2.00, -2.00), Point2(14.83, -2.00), Point2(-2.00, 14.83)]'),
class Container(OpenSCADObject): origin = Connector( Point3(0, 0, 0), Vector3(0, 1, 0), Vector3(0, 0, 1) ) origin_output_connectors = {} def __init__(self, input_connectors=None): OpenSCADObject.__init__(self, "container", {}) self.position = self.origin() self.adjust_to_input(input_connectors) self.transform_matrix = self.generate_transform_matrix() self.add(self.generate()) self.output_connectors = self.generate_output_connectors() def adjust_to_input(self, input_connectors): return def generate_at_origin(self): return union() def generate(self): obj = self.generate_at_origin() matrix = scad_matrix(self.transform_matrix) return multmatrix(m=matrix)(obj) def recursive_transform(self, v): m = self.transform_matrix if not v: return v if isinstance(v, Connector): return v.transform(m) elif isinstance(v, Point3): return m * v elif isinstance(v, Vector3): return m * v elif isinstance(v, dict): new_v = {} for k, v1 in v.items(): new_v[k] = self.recursive_transform(v1) return new_v elif isinstance(v, list): new_v = [] for v1 in v: new_v.append(self.recursive_transform(v1)) return new_v else: raise RuntimeError("Unsupported type") def generate_output_connectors(self): m = self.transform_matrix return self.recursive_transform(self.origin_output_connectors) def generate_transform_matrix(self): m_rotate_base = Matrix4.new_look_at( Point3(0, 0, 0), -self.origin.direction, self.origin.up).inverse() m = Matrix4.new_look_at( Point3(0, 0, 0), -self.position.direction, self.position.up) * m_rotate_base move = self.position.position - self.origin.position m.d, m.h, m.l = move.x, move.y, move.z return m
), ('extrude_along_path', extrude_along_path, [tri, [[0, 0, 0], [0, 20, 0]]], '\n\npolyhedron(faces = [[0, 3, 1], [1, 3, 4], [1, 4, 2], [2, 4, 5], [0, 2, 5], [0, 5, 3], [0, 1, 2], [3, 5, 4]], points = [[0.0000000000, 0.0000000000, 0.0000000000], [10.0000000000, 0.0000000000, 0.0000000000], [0.0000000000, 0.0000000000, 10.0000000000], [0.0000000000, 20.0000000000, 0.0000000000], [10.0000000000, 20.0000000000, 0.0000000000], [0.0000000000, 20.0000000000, 10.0000000000]]);' ), ('extrude_along_path_vertical', extrude_along_path, [tri, [[0, 0, 0], [0, 0, 20]]], '\n\npolyhedron(faces = [[0, 3, 1], [1, 3, 4], [1, 4, 2], [2, 4, 5], [0, 2, 5], [0, 5, 3], [0, 1, 2], [3, 5, 4]], points = [[0.0000000000, 0.0000000000, 0.0000000000], [-10.0000000000, 0.0000000000, 0.0000000000], [0.0000000000, 10.0000000000, 0.0000000000], [0.0000000000, 0.0000000000, 20.0000000000], [-10.0000000000, 0.0000000000, 20.0000000000], [0.0000000000, 10.0000000000, 20.0000000000]]);' ), ] other_test_cases = [ # Test name, function, args, expected value ('euclidify', euclidify, [[0, 0, 0]], 'Vector3(0.00, 0.00, 0.00)'), ('euclidify_recursive', euclidify, [[[0, 0, 0], [1, 0, 0]]], '[Vector3(0.00, 0.00, 0.00), Vector3(1.00, 0.00, 0.00)]'), ('euclidify_Vector', euclidify, [Vector3(0, 0, 0)], 'Vector3(0.00, 0.00, 0.00)'), ('euclidify_recursive_Vector', euclidify, [[Vector3(0, 0, 0), Vector3(0, 0, 1)]], '[Vector3(0.00, 0.00, 0.00), Vector3(0.00, 0.00, 1.00)]'), ('euc_to_arr', euc_to_arr, [Vector3(0, 0, 0)], '[0, 0, 0]'), ('euc_to_arr_recursive', euc_to_arr, [[Vector3(0, 0, 0), Vector3(0, 0, 1)]], '[[0, 0, 0], [0, 0, 1]]'), ('euc_to_arr_arr', euc_to_arr, [[0, 0, 0]], '[0, 0, 0]'), ('euc_to_arr_arr_recursive', euc_to_arr, [[[0, 0, 0], [1, 0, 0]]], '[[0, 0, 0], [1, 0, 0]]'), ('is_scad', is_scad, [cube(2)], 'True'), ('is_scad_false', is_scad, [2], 'False'), ('transform_to_point_single_arr', transform_to_point, [[1, 0, 0], [2, 2, 2], [3, 3, 1]], 'Point3(2.71, 1.29, 2.00)'),
def normal_vector(self) -> Vector3: return Vector3(*self.normal)
def as_vector(self) -> Vector3: return Vector3(*self.point)
def thread(outline_pts: Points, inner_rad: float, pitch: float, length: float, external: bool = True, segments_per_rot: int = 32, neck_in_degrees: float = 0, neck_out_degrees: float = 0, rad_2: float = None, inverse_thread_direction: bool = False): """ Sweeps outline_pts (an array of points describing a closed polygon in XY) through a spiral. :param outline_pts: a list of points (NOT an OpenSCAD polygon) that define the cross section of the thread :type outline_pts: list :param inner_rad: radius of cylinder the screw will wrap around; at base of screw :type inner_rad: number :param pitch: height for one revolution; must be <= the height of outline_pts bounding box to avoid self-intersection :type pitch: number :param length: distance from bottom-most point of screw to topmost :type length: number :param external: if True, the cross-section is external to a cylinder. If False,the segment is internal to it, and outline_pts will be mirrored right-to-left :type external: bool :param segments_per_rot: segments per rotation :type segments_per_rot: int :param neck_in_degrees: degrees through which the outer edge of the screw thread will move from a thickness of zero (inner_rad) to its full thickness :type neck_in_degrees: number :param neck_out_degrees: degrees through which outer edge of the screw thread will move from full thickness back to zero :type neck_out_degrees: number :param rad_2: radius of cylinder the screw will wrap around at top of screw. Defaults to inner_rad :type rad_2: number NOTE: This functions works by creating and returning one huge polyhedron, with potentially thousands of faces. An alternate approach would make one single polyhedron,then repeat it over and over in the spiral shape, unioning them all together. This would create a similar number of SCAD objects and operations, but still require a lot of transforms and unions to be done in the SCAD code rather than in the python, as here. Also would take some doing to make the neck-in work as well. Not sure how the two approaches compare in terms of render-time. -ETJ 16 Mar 2011 NOTE: if pitch is less than or equal to the height of each tooth (outline_pts), OpenSCAD will likely crash, since the resulting screw would self-intersect all over the place. For screws with essentially no space between threads, (i.e., pitch=tooth_height), I use pitch= tooth_height+EPSILON, since pitch=tooth_height will self-intersect for rotations >=1 """ # FIXME: For small segments_per_rot where length is not a multiple of # pitch, the the generated spiral will have irregularities, since we # don't ensure that each level's segments are in line with those above or # below. This would require a change in logic to fix. For now, larger values # of segments_per_rot and length that divides pitch evenly should avoid this issue # -ETJ 02 January 2020 rad_2 = rad_2 or inner_rad rotations = length / pitch total_angle = 360 * rotations up_step = length / (rotations * segments_per_rot) # Add one to total_steps so we have total_steps *segments* total_steps = math.ceil(rotations * segments_per_rot) + 1 step_angle = total_angle / (total_steps - 1) all_points = [] all_tris = [] euc_up = Vector3(*UP_VEC) poly_sides = len(outline_pts) # Make Point3s from outline_pts and flip inward for internal threads int_ext_angle = 0 if external else math.pi outline_pts = [ Point3(p[0], p[1], 0).rotate_around(axis=euc_up, theta=int_ext_angle) for p in outline_pts ] # If this screw is conical, we'll need to rotate tooth profile to # keep it perpendicular to the side of the cone. if inner_rad != rad_2: cone_angle = -math.atan((rad_2 - inner_rad) / length) outline_pts = [ p.rotate_around(axis=Vector3(*UP_VEC), theta=cone_angle) for p in outline_pts ] # outline_pts, since they were created in 2D , are in the XY plane. # But spirals move a profile in XZ around the Z-axis. So swap Y and Z # coordinates... and hope users know about this euc_points = list([Point3(p[0], 0, p[1]) for p in outline_pts]) # Figure out how wide the tooth profile is min_bb, max_bb = bounding_box(outline_pts) outline_w = max_bb[0] - min_bb[0] outline_h = max_bb[1] - min_bb[1] # Calculate where neck-in and neck-out starts/ends neck_out_start = total_angle - neck_out_degrees neck_distance = (outline_w + EPSILON) * (1 if external else -1) section_rads = [ # radius at start of thread max(0, inner_rad - neck_distance), # end of neck-in map_segment(neck_in_degrees, 0, total_angle, inner_rad, rad_2), # start of neck-out map_segment(neck_out_start, 0, total_angle, inner_rad, rad_2), # end of thread (& neck-out) rad_2 - neck_distance ] for i in range(total_steps): angle = i * step_angle elevation = i * up_step if angle > total_angle: angle = total_angle elevation = length # Handle the neck-in radius for internal and external threads if 0 <= angle < neck_in_degrees: rad = map_segment(angle, 0, neck_in_degrees, section_rads[0], section_rads[1]) elif neck_in_degrees <= angle < neck_out_start: rad = map_segment(angle, neck_in_degrees, neck_out_start, section_rads[1], section_rads[2]) elif neck_out_start <= angle <= total_angle: rad = map_segment(angle, neck_out_start, total_angle, section_rads[2], section_rads[3]) elev_vec = Vector3(rad, 0, elevation) # create new points for p in euc_points: theta = radians(angle) * (-1 if inverse_thread_direction else 1) pt = (p + elev_vec).rotate_around(axis=euc_up, theta=theta) all_points.append(pt.as_arr()) # Add the connectivity information if i < total_steps - 1: ind = i * poly_sides for j in range(ind, ind + poly_sides - 1): all_tris.append([j, j + 1, j + poly_sides]) all_tris.append([j + 1, j + poly_sides + 1, j + poly_sides]) all_tris.append( [ind, ind + poly_sides - 1 + poly_sides, ind + poly_sides - 1]) all_tris.append( [ind, ind + poly_sides, ind + poly_sides - 1 + poly_sides]) # End triangle fans for beginning and end last_loop = len(all_points) - poly_sides for i in range(poly_sides - 2): all_tris.append([0, i + 2, i + 1]) all_tris.append([last_loop, last_loop + i + 1, last_loop + i + 2]) # Moving in the opposite direction, we need to reverse the order of # corners in each face so the OpenSCAD preview renders correctly if inverse_thread_direction: all_tris = list([reversed(trio) for trio in all_tris]) # Make the polyhedron; convexity info needed for correct OpenSCAD render a = polyhedron(points=all_points, faces=all_tris, convexity=2) if external: # Intersect with a cylindrical tube to make sure we fit into # the correct dimensions tube = cylinder(r1=inner_rad + outline_w + EPSILON, r2=rad_2 + outline_w + EPSILON, h=length, segments=segments_per_rot) tube -= cylinder(r1=inner_rad, r2=rad_2, h=length, segments=segments_per_rot) else: # If the threading is internal, intersect with a central cylinder # to make sure nothing else remains tube = cylinder(r1=inner_rad, r2=rad_2, h=length, segments=segments_per_rot) a *= tube return a
def _rotate_loop(points:Sequence[Point3], rotation_degrees:float=None) -> List[Point3]: if rotation_degrees is None: return points up = Vector3(0,0,1) rads = radians(rotation_degrees) return [p.rotate_around(up, rads) for p in points]
def extrude_along_path( shape_pts:Points, path_pts:Points, scale_factors:Sequence[float]=None) -> OpenSCADObject: # Extrude the convex 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. # # -- len(scale_factors) should equal len(path_pts). If not present, scale # will be assumed to be 1.0 for each point in path_pts # -- Future additions might include corner styles (sharp, flattened, round) # or a twist factor polyhedron_pts:Points= [] facet_indices:List[Tuple[int, int, int]] = [] if not scale_factors: scale_factors = [1.0] * len(path_pts) # 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(*UP_VEC) for which_loop in range(len(path_pts)): path_pt = path_pts[which_loop] scale = scale_factors[which_loop] # calculate the tangent to the curve at this point if which_loop > 0 and which_loop < len(path_pts) - 1: prev_pt = path_pts[which_loop - 1] next_pt = path_pts[which_loop + 1] v_prev = path_pt - prev_pt v_next = next_pt - path_pt tangent = v_prev + v_next elif which_loop == 0: tangent = path_pts[which_loop + 1] - path_pt elif which_loop == len(path_pts) - 1: tangent = path_pt - path_pts[which_loop - 1] # Scale points this_loop:Point3 = [] if scale != 1.0: this_loop = [(scale * sh) for sh in shape_pts] # Convert this_loop back to points; scaling changes them to Vectors this_loop = [Point3(v.x, v.y, v.z) for v in this_loop] else: this_loop = shape_pts[:] # type: ignore # Rotate & translate this_loop = transform_to_point(this_loop, dest_point=path_pt, dest_normal=tangent, src_up=src_up) # Add the transformed points to our final list polyhedron_pts += this_loop # And calculate the facet indices shape_pt_count = len(shape_pts) segment_start = which_loop * shape_pt_count segment_end = segment_start + shape_pt_count - 1 if which_loop < len(path_pts) - 1: for i in range(segment_start, segment_end): facet_indices.append( (i, i + shape_pt_count, i + 1) ) facet_indices.append( (i + 1, i + shape_pt_count, i + shape_pt_count + 1) ) facet_indices.append( (segment_start, segment_end, segment_end + shape_pt_count) ) facet_indices.append( (segment_start, segment_end + shape_pt_count, segment_start + shape_pt_count) ) # Cap the start of the polyhedron for i in range(1, shape_pt_count - 1): facet_indices.append((0, i, i + 1)) # And the end (could be rolled into the earlier loop) # FIXME: concave cross-sections will cause this end-capping algorithm # to fail end_cap_base = len(polyhedron_pts) - shape_pt_count for i in range(end_cap_base + 1, len(polyhedron_pts) - 1): facet_indices.append( (end_cap_base, i + 1, i) ) return polyhedron(points=euc_to_arr(polyhedron_pts), faces=facet_indices) # type: ignore
def thread(outline_pts: Points, inner_rad: float, pitch: float, length: float, external: bool = True, segments_per_rot: int = 32, neck_in_degrees: float = 0, neck_out_degrees: float = 0): """ Sweeps outline_pts (an array of points describing a closed polygon in XY) through a spiral. :param outline_pts: a list of points (NOT an OpenSCAD polygon) that define the cross section of the thread :type outline_pts: list :param inner_rad: radius of cylinder the screw will wrap around :type inner_rad: number :param pitch: height for one revolution; must be <= the height of outline_pts bounding box to avoid self-intersection :type pitch: number :param length: distance from bottom-most point of screw to topmost :type length: number :param external: if True, the cross-section is external to a cylinder. If False,the segment is internal to it, and outline_pts will be mirrored right-to-left :type external: bool :param segments_per_rot: segments per rotation :type segments_per_rot: int :param neck_in_degrees: degrees through which the outer edge of the screw thread will move from a thickness of zero (inner_rad) to its full thickness :type neck_in_degrees: number :param neck_out_degrees: degrees through which outer edge of the screw thread will move from full thickness back to zero :type neck_out_degrees: number NOTE: This functions works by creating and returning one huge polyhedron, with potentially thousands of faces. An alternate approach would make one single polyhedron,then repeat it over and over in the spiral shape, unioning them all together. This would create a similar number of SCAD objects and operations, but still require a lot of transforms and unions to be done in the SCAD code rather than in the python, as here. Also would take some doing to make the neck-in work as well. Not sure how the two approaches compare in terms of render-time. -ETJ 16 Mar 2011 NOTE: if pitch is less than or equal to the height of each tooth (outline_pts), OpenSCAD will likely crash, since the resulting screw would self-intersect all over the place. For screws with essentially no space between threads, (i.e., pitch=tooth_height), I use pitch= tooth_height+EPSILON, since pitch=tooth_height will self-intersect for rotations >=1 """ rotations = length / pitch total_angle = 360 * rotations up_step = length / (rotations * segments_per_rot) # Add one to total_steps so we have total_steps *segments* total_steps = ceil(rotations * segments_per_rot) + 1 step_angle = total_angle / (total_steps - 1) all_points = [] all_tris = [] euc_up = Vector3(*UP_VEC) poly_sides = len(outline_pts) # Figure out how wide the tooth profile is min_bb, max_bb = bounding_box(outline_pts) outline_w = max_bb[0] - min_bb[0] outline_h = max_bb[1] - min_bb[1] min_rad = max(0, inner_rad - outline_w - EPSILON) max_rad = inner_rad + outline_w + EPSILON # outline_pts, since they were created in 2D , are in the XY plane. # But spirals move a profile in XZ around the Z-axis. So swap Y and Z # coordinates... and hope users know about this # Also add inner_rad to the profile euc_points = [] for p in outline_pts: # If p is in [x, y] format, make it [x, y, 0] if len(p) == 2: p.append(0) # [x, y, z] => [ x+inner_rad, z, y] external_mult = 1 if external else -1 # adding inner_rad, swapping Y & Z s = Point3(external_mult * p[0], p[2], p[1]) euc_points.append(s) for i in range(total_steps): angle = i * step_angle elevation = i * up_step if angle > total_angle: angle = total_angle elevation = length # Handle the neck-in radius for internal and external threads rad = inner_rad int_ext_mult = 1 if external else -1 neck_in_rad = min_rad if external else max_rad if neck_in_degrees != 0 and angle < neck_in_degrees: rad = neck_in_rad + int_ext_mult * angle / neck_in_degrees * outline_w elif neck_out_degrees != 0 and angle > total_angle - neck_out_degrees: rad = neck_in_rad + int_ext_mult * ( total_angle - angle) / neck_out_degrees * outline_w elev_vec = Vector3(rad, 0, elevation) # create new points for p in euc_points: pt = (p + elev_vec).rotate_around(axis=euc_up, theta=radians(angle)) all_points.append(pt.as_arr()) # Add the connectivity information if i < total_steps - 1: ind = i * poly_sides for j in range(ind, ind + poly_sides - 1): all_tris.append([j, j + 1, j + poly_sides]) all_tris.append([j + 1, j + poly_sides + 1, j + poly_sides]) all_tris.append( [ind, ind + poly_sides - 1 + poly_sides, ind + poly_sides - 1]) all_tris.append( [ind, ind + poly_sides, ind + poly_sides - 1 + poly_sides]) # End triangle fans for beginning and end last_loop = len(all_points) - poly_sides for i in range(poly_sides - 2): all_tris.append([0, i + 2, i + 1]) all_tris.append([last_loop, last_loop + i + 1, last_loop + i + 2]) # Make the polyhedron a = polyhedron(points=all_points, faces=all_tris) if external: # Intersect with a cylindrical tube to make sure we fit into # the correct dimensions tube = cylinder(r=inner_rad + outline_w + EPSILON, h=length, segments=segments_per_rot) tube -= cylinder(r=inner_rad, h=length, segments=segments_per_rot) else: # If the threading is internal, intersect with a central cylinder # to make sure nothing else remains tube = cylinder(r=inner_rad, h=length, segments=segments_per_rot) a *= tube return render()(a)
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