def linearpath(x, y, z): outline = [ Point3(0, 0, 0), Point3(0, 0, 10), Point3(0, 20, 10), Point3(x, y, z) ] return outline
def test_bezier_points(self): expected = [ Point3(0.00, 0.00), Point3(1.38, 0.62), Point3(2.00, -1.00) ] actual = bezier_points(self.bezier_controls, subdivisions=self.subdivisions) self.assertPointsListsEqual(expected, actual)
def test_bezier_points_raw(self): # Verify that we can use raw sequences of floats as inputs (e.g [(1,2), (3.2,4)]) # rather than sequences of Point2s expected = [ Point3(0.00, 0.00), Point3(1.38, 0.62), Point3(2.00, -1.00) ] actual = bezier_points(self.bezier_controls_raw, subdivisions=self.subdivisions) self.assertPointsListsEqual(expected, actual)
def test_catmull_rom_points(self): expected = [ Point3(0.00, 0.00), Point3(0.38, 0.44), Point3(1.00, 1.00), Point3(1.62, 1.06), Point3(2.00, 1.00) ] actual = catmull_rom_points(self.points, subdivisions=self.subdivisions, close_loop=False) self.assertPointsListsEqual(expected, actual)
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
def test_catmull_rom_points_raw(self): # Verify that we can use raw sequences of floats as inputs (e.g [(1,2), (3.2,4)]) # rather than sequences of Point2s expected = [ Point3(0.00, 0.00), Point3(0.38, 0.44), Point3(1.00, 1.00), Point3(1.62, 1.06), Point3(2.00, 1.00) ] actual = catmull_rom_points(self.points_raw, subdivisions=self.subdivisions, close_loop=False) self.assertPointsListsEqual(expected, actual)
def round_point(point: Point3, n_digits=2): """Round a given 3d point.""" p = point.copy() p.x = round(p.x, n_digits) p.y = round(p.y, n_digits) p.z = round(p.z, n_digits) return p.z
def draw_segment(euc_line=None, endless=False, arrow_rad=7, vec_color=None): # Draw a tradtional arrow-head vector in 3-space. vec_arrow_rad = arrow_rad vec_arrow_head_rad = vec_arrow_rad * 1.5 vec_arrow_head_length = vec_arrow_rad * 3 if isinstance(euc_line, Vector3): p = Point3(*ORIGIN) v = euc_line elif isinstance(euc_line, Line3): p = euc_line.p v = -euc_line.v elif isinstance(euc_line, list) or isinstance(euc_line, tuple): # TODO: This assumes p & v are PyEuclid classes. # Really, they could as easily be two 3-tuples. Should # check for this. p, v = euc_line[0], euc_line[1] shaft_length = v.magnitude() - vec_arrow_head_length arrow = cylinder(r=vec_arrow_rad, h=shaft_length) arrow += up(shaft_length)(cylinder(r1=vec_arrow_head_rad, r2=0, h=vec_arrow_head_length)) if endless: endless_length = max(v.magnitude() * 10, 200) arrow += cylinder(r=vec_arrow_rad / 3, h=endless_length, center=True) arrow = transform_to_point(body=arrow, dest_point=p, dest_normal=v) if vec_color: arrow = color(vec_color)(arrow) return arrow
def star(num_points=5, outer_rad=15, dip_factor=0.5): star_pts = [] for i in range(2 * num_points): rad = outer_rad - i % 2 * dip_factor * outer_rad angle = radians(360 / (2 * num_points) * i) star_pts.append(Point3(rad * cos(angle), rad * sin(angle), 0)) return star_pts
def _scale_loop(points:Sequence[Point3], scale:Union[float, Point2, Tuple2]=None) -> List[Point3]: if scale is None: return points if isinstance(scale, (float, int)): scale = [scale] * 2 return [Point3(point.x * scale[0], point.y * scale[1], point.z) for point in points]
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 sinusoidal_ring(rad=25, segments=SEGMENTS): outline = [] for i in range(segments): angle = i * 360 / segments x = rad * cos(radians(angle)) y = rad * sin(radians(angle)) z = 2 * sin(radians(angle * 6)) outline.append(Point3(x, y, z)) return outline
def test_catmull_rom_points_3d(self): points = [Point3(-1, -1, 0), Point3(0, 0, 1), Point3(1, 1, 0)] expected = [ Point3(-1.00, -1.00, 0.00), Point3(-0.62, -0.62, 0.50), Point3(0.00, 0.00, 1.00), Point3(0.62, 0.62, 0.50), Point3(1.00, 1.00, 0.00) ] actual = catmull_rom_points(points, subdivisions=2) self.assertPointsListsEqual(expected, actual)
def create_cutter(addr, left_or_right_dir): pos = switches.get_switch_position(addr) pos = Point3(*pos, 0) angle = switches.get_switch_angle(addr) cutter = switches.create_switch(addr, size=1) cutter = sc.translate(pos)(sc.scale( (4, 4, 0))(sc.translate(pos * -1)(cutter))) offset = utils.unit_point2(angle) * 2.5 * left_or_right_dir cutter = sc.translate((*offset, 0))(cutter) return cutter
def sinusoidal_ring(rad=25, segments=SEGMENTS) -> List[Point3]: outline = [] for i in range(segments): angle = radians(i * 360 / segments) scaled_rad = (1 + 0.18 * cos(angle * 5)) * rad x = scaled_rad * cos(angle) y = scaled_rad * sin(angle) z = 0 # Or stir it up and add an oscillation in z as well # z = 3 * sin(angle * 6) outline.append(Point3(x, y, z)) return outline
def createseg(p1,p2,t): linearpath = [Point3(0,0,0), Point3(0,0,t)] #calculations for the four corners of the face #alpha is the angle of the segment from the x-axis CCW if p2[0] == p1[1]: alpha = np.pi/2 alpha = np.arctan2((p2[1]-p1[1]),(p2[0]-p1[0])) #the x displacement of the corner from p1 and p2 xoff = math.sin(alpha)*(t/2) #the y displacement of the corner from p1 and p2 yoff = math.cos(alpha)*(t/2) #1Acorner A1corner = [p1[0]-xoff,p1[1]+yoff] #B1corner B1corner = [p1[0]+xoff,p1[1]-yoff] #A2corner A2corner = [p2[0]-xoff,p2[1]+yoff] #B2corner B2corner = [p2[0]+xoff,p2[1]-yoff] #make the face from the 4 points that were defined earlier. face_pts = [Point3(A1corner[0], A1corner[1],0)] face_pts.append(Point3(A2corner[0], A2corner[1],0)) face_pts.append(Point3(B2corner[0], B2corner[1],0)) face_pts.append(Point3(B1corner[0], B1corner[1],0)) #creates segment seg = extrude_along_path(shape_pts=face_pts,path_pts=linearpath) return seg
def face(start, sidelength, plane): #start = starting point as an array [0,0,0] #sidelength = int value #plane = decide which plane your face is on ('xy' for xy plane, etc..) face_pts = [Point3(start[0], start[1], start[2])] if plane == 'xy': face_pts.append(Point3(start[0] - sidelength, start[1], start[2])) face_pts.append( Point3(start[0] - sidelength, start[1] + sidelength, start[2])) face_pts.append(Point3(start[0], start[1] + sidelength, start[2])) elif plane == 'yz': face_pts.append(Point3(start[0], start[1] + sidelength, start[2])) face_pts.append( Point3(start[0], start[1] + sidelength, start[2] + sidelength)) face_pts.append(Point3(start[0], start[1], start[2] + sidelength)) elif plane == 'xz': face_pts.append(Point3(start[0] + sidelength, start[1], start[2])) face_pts.append( Point3(start[0] + sidelength, start[1] + sidelength, start[2])) face_pts.append(Point3(start[0], start[1] + sidelength, start[2])) return face_pts
def affine_combination(a, b, weight=0.5): """ Note that weight is a fraction of the distance between self and other. So... 0.33 is a point .33 of the way between self and other. """ if hasattr(a, 'z'): return Point3( (1 - weight) * a.x + weight * b.x, (1 - weight) * a.y + weight * b.y, (1 - weight) * a.z + weight * b.z, ) else: return Point2( (1 - weight) * a.x + weight * b.x, (1 - weight) * a.y + weight * b.y, )
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 toroidial_helix_coil(rad=25.4-2, pitch = (2*pi*((6*25.4)/2-(25.4-2/2)-2))/30, outer_rad=(6*25.4)/2-(25.4-2/2)-2, segments=SEGMENTS): h = 2*pi*outer_rad a = rad b = pitch/(2*pi) outline = [] for i in range(segments): theta = 2*pi*(i/segments) t = (i/segments*h)/b x = a*cos(t)+outer_rad y = a*sin(t) z = 0#b*t # then rotate x, z = x*cos(theta) - z*sin(theta), x*sin(theta) + z*cos(theta) outline.append(Point3(x, y, z)) return outline
def test_bezier_points_3d(self): # verify that we get a valid bezier curve back even when its control points # are outside the XY plane and aren't coplanar controls_3d = [ Point3(-2, -1, 0), Point3(-0.5, -0.5, 1), Point3(0.5, 0.5, 1), Point3(2, 1, 0) ] actual = bezier_points(controls_3d, subdivisions=self.subdivisions) expected = [ Point3(-2.00, -1.00, 0.00), Point3(0.00, 0.00, 0.75), Point3(2.00, 1.00, 0.00) ] self.assertPointsListsEqual(expected, actual)
def kochify_3d(a, b, c, ab_weight=0.5, bc_weight=0.5, ca_weight=0.5, pyr_a_weight=ONE_THIRD, pyr_b_weight=ONE_THIRD, pyr_c_weight=ONE_THIRD, pyr_height_weight=ONE_THIRD): """ Point3s a, b, and c must be coplanar and define a face ab_weight, etc define the subdivision of the original face pyr_a_weight, etc define where the point of the new pyramid face will go pyr_height determines how far from the face the new pyramid's point will be """ triangles = [] new_a = affine_combination(a, b, ab_weight) new_b = affine_combination(b, c, bc_weight) new_c = affine_combination(c, a, ca_weight) triangles.extend([[a, new_a, new_c], [b, new_b, new_a], [c, new_c, new_b]]) avg_pt_x = a.x * pyr_a_weight + b.x * pyr_b_weight + c.x * pyr_c_weight avg_pt_y = a.y * pyr_a_weight + b.y * pyr_b_weight + c.y * pyr_c_weight avg_pt_z = a.z * pyr_a_weight + b.z * pyr_b_weight + c.z * pyr_c_weight center_pt = Point3(avg_pt_x, avg_pt_y, avg_pt_z) # The top of the pyramid will be on a normal ab_vec = b - a bc_vec = c - b ca_vec = a - c normal = ab_vec.cross(bc_vec).normalized() avg_side_length = (abs(ab_vec) + abs(bc_vec) + abs(ca_vec)) / 3 pyr_h = avg_side_length * pyr_height_weight pyr_pt = LineSegment3(center_pt, normal, pyr_h).p2 triangles.extend([[new_a, pyr_pt, new_c], [new_b, pyr_pt, new_a], [new_c, pyr_pt, new_b]]) return triangles
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 setUp(self): self.points = [ Point3(0, 0), Point3(1, 1), Point3(2, 1), ] self.points_raw = [ (0, 0), (1, 1), (2, 1), ] self.bezier_controls = [ Point3(0, 0), Point3(1, 1), Point3(2, 1), Point3(2, -1), ] self.bezier_controls_raw = [(0, 0), (1, 1), (2, 1), (2, -1)] self.subdivisions = 2
def linearpath(start, extrudelength, direction): if direction == 'x': outline = [ Point3(start[0], start[1], start[2]), Point3(start[0] + extrudelength, start[1], start[2]) ] elif direction == 'y': outline = [ Point3(start[0], start[1] + 10, start[2]), Point3(start[0], start[1] + extrudelength, start[2]) ] elif direction == 'z': outline = [ Point3(start[0], start[1], start[2]), Point3(start[0], start[1], start[2] + extrudelength) ] return outline
import difflib import unittest import re from euclid3 import Point3, Vector3, Point2 from solid import scad_render from solid.objects import cube, polygon, sphere, translate from solid.test.ExpandedTestCase import DiffOutput from solid.utils import BoundingBox, arc, arc_inverted, euc_to_arr, euclidify from solid.utils import extrude_along_path, fillet_2d, is_scad, offset_points from solid.utils import split_body_planar, transform_to_point, project_to_2D from solid.utils import FORWARD_VEC, RIGHT_VEC, UP_VEC from solid.utils import back, down, forward, left, right, up from solid.utils import label tri = [Point3(0, 0, 0), Point3(10, 0, 0), Point3(0, 10, 0)] scad_test_cases = [ # Test name, function, args, expected value ('up', up, [2], '\n\ntranslate(v = [0, 0, 2]);'), ('down', down, [2], '\n\ntranslate(v = [0, 0, -2]);'), ('left', left, [2], '\n\ntranslate(v = [-2, 0, 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}' ),
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
import unittest import re from euclid3 import Point3, Vector3, Point2 from solid import scad_render from solid.objects import cube, polygon, sphere, translate from solid.test.ExpandedTestCase import DiffOutput from solid.utils import BoundingBox, arc, arc_inverted, euc_to_arr, euclidify from solid.utils import extrude_along_path, fillet_2d, is_scad, offset_points from solid.utils import split_body_planar, transform_to_point, project_to_2D from solid.utils import path_2d, path_2d_polygon from solid.utils import FORWARD_VEC, RIGHT_VEC, UP_VEC from solid.utils import back, down, forward, left, right, up from solid.utils import label tri = [Point3(0, 0, 0), Point3(10, 0, 0), Point3(0, 10, 0)] scad_test_cases = [ # Test name, function, args, expected value ('up', up, [2], '\n\ntranslate(v = [0, 0, 2]);'), ('down', down, [2], '\n\ntranslate(v = [0, 0, -2]);'), ('left', left, [2], '\n\ntranslate(v = [-2, 0, 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]]);'),
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
#! /usr/bin/env python3 import unittest import re from solid import OpenSCADObject, scad_render from solid.utils import extrude_along_path from euclid3 import Point2, Point3 from typing import Union tri = [Point3(0, 0, 0), Point3(10, 0, 0), Point3(0, 10, 0)] class TestExtrudeAlongPath(unittest.TestCase): # Test cases will be dynamically added to this instance # using the test case arrays above def assertEqualNoWhitespace(self, a, b): remove_whitespace = lambda s: re.subn(r'[\s\n]', '', s)[0] self.assertEqual(remove_whitespace(a), remove_whitespace(b)) def assertEqualOpenScadObject(self, expected: str, actual: Union[OpenSCADObject, str]): if isinstance(actual, OpenSCADObject): act = scad_render(actual) elif isinstance(actual, str): act = actual self.assertEqualNoWhitespace(expected, act) def test_extrude_along_path(self): path = [[0, 0, 0], [0, 20, 0]] # basic test