Esempio n. 1
0
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)
Esempio n. 3
0
    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)
Esempio n. 5
0
    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)
Esempio n. 6
0
 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)
Esempio n. 7
0
    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)
Esempio n. 9
0
    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')
Esempio n. 10
0
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
Esempio n. 11
0
    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)
Esempio n. 12
0
    ('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)]'),
Esempio n. 13
0
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
Esempio n. 14
0
     ),
    ('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)'),
Esempio n. 15
0
 def normal_vector(self) -> Vector3:
     return Vector3(*self.normal)
Esempio n. 16
0
 def as_vector(self) -> Vector3:
     return Vector3(*self.point)
Esempio n. 17
0
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
Esempio n. 18
0
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]
Esempio n. 19
0
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
Esempio n. 20
0
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)
Esempio n. 21
0
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