1# Copyright (c) 2018-2021 Manfred Moitzi 2# License: MIT License 3from typing import TYPE_CHECKING, Iterable, List, Tuple, Sequence 4from math import pi, sin, cos, radians, tan, isclose, asin, fabs 5from enum import IntEnum 6from ezdxf.math import ( 7 Vec3, Matrix44, global_bspline_interpolation, EulerSpiral, 8) 9from ezdxf.render.mesh import MeshVertexMerger, MeshTransformer 10 11if TYPE_CHECKING: 12 from ezdxf.eztypes import Vertex 13 14__all__ = [ 15 "circle", "ellipse", "euler_spiral", "square", "box", "open_arrow", 16 "arrow2", "ngon", "star", "gear", "translate", "rotate", "scale", 17 "close_polygon", "cube", "extrude", "cylinder", "cylinder_2p", 18 "from_profiles_linear", "from_profiles_spline", "spline_interpolation", 19 "spline_interpolated_profiles", "cone", "cone_2p", "rotation_form", 20 "sphere", 21] 22 23 24def circle(count: int, radius: float = 1, elevation: float = 0, 25 close: bool = False) -> Iterable[Vec3]: 26 """ Create polygon vertices for a `circle <https://en.wikipedia.org/wiki/Circle>`_ 27 with the given `radius` and approximated by `count` vertices, `elevation` 28 is the z-axis for all vertices. 29 30 Args: 31 count: count of polygon vertices 32 radius: circle radius 33 elevation: z-axis for all vertices 34 close: yields first vertex also as last vertex if ``True``. 35 36 Returns: 37 vertices in counter clockwise orientation as :class:`~ezdxf.math.Vec3` 38 objects 39 40 """ 41 radius = float(radius) 42 delta = 2. * pi / count 43 alpha = 0. 44 for index in range(count): 45 x = cos(alpha) * radius 46 y = sin(alpha) * radius 47 yield Vec3(x, y, elevation) 48 alpha += delta 49 50 if close: 51 yield Vec3(radius, 0, elevation) 52 53 54def ellipse(count: int, rx: float = 1, ry: float = 1, start_param: float = 0, 55 end_param: float = 2 * pi, elevation: float = 0) -> Iterable[Vec3]: 56 """ Create polygon vertices for an `ellipse <https://en.wikipedia.org/wiki/Ellipse>`_ 57 with given `rx` as x-axis radius and `ry` as y-axis radius approximated by 58 `count` vertices, `elevation` is the z-axis for all vertices. 59 The ellipse goes from `start_param` to `end_param` in counter clockwise 60 orientation. 61 62 Args: 63 count: count of polygon vertices 64 rx: ellipse x-axis radius 65 ry: ellipse y-axis radius 66 start_param: start of ellipse in range [0, 2π] 67 end_param: end of ellipse in range [0, 2π] 68 elevation: z-axis for all vertices 69 70 Returns: 71 vertices in counter clockwise orientation as :class:`~ezdxf.math.Vec3` 72 objects 73 74 """ 75 rx = float(rx) 76 ry = float(ry) 77 start_param = float(start_param) 78 end_param = float(end_param) 79 count = int(count) 80 delta = (end_param - start_param) / (count - 1) 81 for param in range(count): 82 alpha = start_param + param * delta 83 yield Vec3(cos(alpha) * rx, sin(alpha) * ry, elevation) 84 85 86def euler_spiral(count: int, length: float = 1, curvature: float = 1, 87 elevation: float = 0) -> Iterable[Vec3]: 88 """ Create polygon vertices for an `euler spiral <https://en.wikipedia.org/wiki/Euler_spiral>`_ 89 of a given `length` and radius of curvature. This is a parametric curve, 90 which always starts at the origin (0, 0). 91 92 Args: 93 count: count of polygon vertices 94 length: length of curve in drawing units 95 curvature: radius of curvature 96 elevation: z-axis for all vertices 97 98 Returns: 99 vertices as :class:`~ezdxf.math.Vec3` objects 100 101 """ 102 spiral = EulerSpiral(curvature=curvature) 103 for vertex in spiral.approximate(length, count - 1): 104 yield vertex.replace(z=elevation) 105 106 107def square(size: float = 1.) -> Tuple[Vec3, Vec3, Vec3, Vec3]: 108 """ Returns 4 vertices for a square with a side length of the given `size`, 109 lower left corner is ``(0, 0)``, upper right corner is (`size`, `size`). 110 111 """ 112 return Vec3(0, 0), Vec3(size, 0), Vec3(size, size), Vec3(0, size) 113 114 115def box(sx: float = 1., sy: float = 1.) -> Tuple[Vec3, Vec3, Vec3, Vec3]: 116 """ Returns 4 vertices for a box with a width of `sx` by and a height of 117 `sy`, lower left corner is ``(0, 0)``, upper right corner is (`sx`, `sy`). 118 119 """ 120 return Vec3(0, 0), Vec3(sx, 0), Vec3(sx, sy), Vec3(0, sy) 121 122 123def open_arrow(size: float = 1., angle: float = 30.) -> Tuple[Vec3, Vec3, Vec3]: 124 """ Returns 3 vertices for an open arrow `<` with a length of the given 125 `size`, argument `angle` defines the enclosing angle in degrees. 126 Vertex order: upward end vertex, tip (0, 0) , downward end vertex (counter 127 clockwise order) 128 129 Args: 130 size: length of arrow 131 angle: enclosing angle in degrees 132 133 """ 134 h = sin(radians(angle / 2.)) * size 135 return Vec3(-size, h), Vec3(0, 0), Vec3(-size, -h) 136 137 138def arrow2(size: float = 1., angle: float = 30., beta: float = 45.) -> Tuple[ 139 Vec3, Vec3, Vec3, Vec3]: 140 """ Returns 4 vertices for an arrow with a length of the given `size`, and 141 an enclosing `angle` in degrees and a slanted back side defined by angle 142 `beta`:: 143 144 **** 145 **** * 146 **** * 147 **** angle X******************** 148 **** * +beta 149 **** * 150 **** 151 152 **** 153 **** * 154 **** * 155 **** angle X*************** 156 **** * -beta 157 **** * 158 **** 159 160 Vertex order: upward end vertex, tip (0, 0), downward end vertex, bottom 161 vertex `X` (anti clockwise order). 162 163 Bottom vertex `X` is also the connection point to a continuation line. 164 165 Args: 166 size: length of arrow 167 angle: enclosing angle in degrees 168 beta: angle if back side in degrees 169 170 """ 171 h = sin(radians(angle / 2.)) * size 172 back_step = tan(radians(beta)) * h 173 return Vec3(-size, h), Vec3(0, 0), Vec3(-size, -h), \ 174 Vec3(-size + back_step, 0) 175 176 177def ngon(count: int, length: float = None, radius: float = None, 178 rotation: float = 0., 179 elevation: float = 0., close: bool = False) -> Iterable[Vec3]: 180 """ Returns the corner vertices of a `regular polygon <https://en.wikipedia.org/wiki/Regular_polygon>`_. 181 The polygon size is determined by the edge `length` or the circum `radius` 182 argument. If both are given `length` has the higher priority. 183 184 Args: 185 count: count of polygon corners >= 3 186 length: length of polygon side 187 radius: circum radius 188 rotation: rotation angle in radians 189 elevation: z-axis for all vertices 190 close: yields first vertex also as last vertex if ``True``. 191 192 Returns: 193 vertices as :class:`~ezdxf.math.Vec3` objects 194 195 """ 196 if count < 3: 197 raise ValueError('Argument `count` has to be greater than 2.') 198 if length is not None: 199 if length <= 0.: 200 raise ValueError('Argument `length` has to be greater than 0.') 201 radius = length / 2. / sin(pi / count) 202 elif radius is not None: 203 if radius <= 0.: 204 raise ValueError('Argument `radius` has to be greater than 0.') 205 else: 206 raise ValueError('Argument `length` or `radius` required.') 207 208 delta = 2. * pi / count 209 angle = rotation 210 first = None 211 for _ in range(count): 212 v = Vec3(radius * cos(angle), radius * sin(angle), elevation) 213 if first is None: 214 first = v 215 yield v 216 angle += delta 217 218 if close: 219 yield first 220 221 222def star(count: int, r1: float, r2: float, rotation: float = 0., 223 elevation: float = 0., close: bool = False) -> Iterable[Vec3]: 224 """ Returns the corner vertices for a `star shape <https://en.wikipedia.org/wiki/Star_polygon>`_. 225 226 The shape has `count` spikes, `r1` defines the radius of the "outer" 227 vertices and `r2` defines the radius of the "inner" vertices, 228 but this does not mean that `r1` has to be greater than `r2`. 229 230 Args: 231 count: spike count >= 3 232 r1: radius 1 233 r2: radius 2 234 rotation: rotation angle in radians 235 elevation: z-axis for all vertices 236 close: yields first vertex also as last vertex if ``True``. 237 238 Returns: 239 vertices as :class:`~ezdxf.math.Vec3` objects 240 241 """ 242 if count < 3: 243 raise ValueError('Argument `count` has to be greater than 2.') 244 if r1 <= 0.: 245 raise ValueError('Argument `r1` has to be greater than 0.') 246 if r2 <= 0.: 247 raise ValueError('Argument `r2` has to be greater than 0.') 248 249 corners1 = ngon(count, radius=r1, rotation=rotation, elevation=elevation, 250 close=False) 251 corners2 = ngon(count, radius=r2, rotation=pi / count + rotation, 252 elevation=elevation, close=False) 253 first = None 254 for s1, s2 in zip(corners1, corners2): 255 if first is None: 256 first = s1 257 yield s1 258 yield s2 259 260 if close: 261 yield first 262 263 264class _Gear(IntEnum): 265 TOP_START = 0 266 TOP_END = 1 267 BOTTOM_START = 2 268 BOTTOM_END = 3 269 270 271def gear(count: int, top_width: float, bottom_width: float, height: float, 272 outside_radius: float, elevation: float = 0, 273 close: bool = False) -> Iterable[Vec3]: 274 """ Returns the corner vertices of a `gear shape <https://en.wikipedia.org/wiki/Gear>`_ 275 (cogwheel). 276 277 .. warning:: 278 279 This function does not create correct gears for mechanical engineering! 280 281 Args: 282 count: teeth count >= 3 283 top_width: teeth width at outside radius 284 bottom_width: teeth width at base radius 285 height: teeth height; base radius = outside radius - height 286 outside_radius: outside radius 287 elevation: z-axis for all vertices 288 close: yields first vertex also as last vertex if True. 289 290 Returns: 291 vertices in counter clockwise orientation as :class:`~ezdxf.math.Vec3` 292 objects 293 294 """ 295 if count < 3: 296 raise ValueError('Argument `count` has to be greater than 2.') 297 if outside_radius <= 0.: 298 raise ValueError('Argument `radius` has to be greater than 0.') 299 if top_width <= 0.: 300 raise ValueError('Argument `width` has to be greater than 0.') 301 if bottom_width <= 0.: 302 raise ValueError('Argument `width` has to be greater than 0.') 303 if height <= 0.: 304 raise ValueError('Argument `height` has to be greater than 0.') 305 if height >= outside_radius: 306 raise ValueError('Argument `height` has to be smaller than `radius`') 307 308 base_radius = outside_radius - height 309 alpha_top = asin(top_width / 2. / outside_radius) # angle at tooth top 310 alpha_bottom = asin( 311 bottom_width / 2. / base_radius) # angle at tooth bottom 312 alpha_difference = ( 313 alpha_bottom - alpha_top) / 2. # alpha difference at start and end of tooth 314 beta = (2. * pi - count * alpha_bottom) / count 315 angle = -alpha_top / 2. # center of first tooth is in x-axis direction 316 state = _Gear.TOP_START 317 first = None 318 for _ in range(4 * count): 319 if state == _Gear.TOP_START or state == _Gear.TOP_END: 320 radius = outside_radius 321 else: 322 radius = base_radius 323 v = Vec3(radius * cos(angle), radius * sin(angle), elevation) 324 325 if state == _Gear.TOP_START: 326 angle += alpha_top 327 elif state == _Gear.TOP_END: 328 angle += alpha_difference 329 elif state == _Gear.BOTTOM_START: 330 angle += beta 331 elif state == _Gear.BOTTOM_END: 332 angle += alpha_difference 333 334 if first is None: 335 first = v 336 yield v 337 338 state += 1 339 if state > _Gear.BOTTOM_END: 340 state = _Gear.TOP_START 341 342 if close: 343 yield first 344 345 346def translate(vertices: Iterable['Vertex'], vec: 'Vertex' = (0, 0, 0)) -> \ 347 Iterable[Vec3]: 348 """ Translate `vertices` along `vec`, faster than a Matrix44 transformation. 349 350 Args: 351 vertices: iterable of vertices 352 vec: translation vector 353 354 Returns: yields transformed vertices 355 356 """ 357 vec = Vec3(vec) 358 for p in vertices: 359 yield vec + p 360 361 362def rotate(vertices: Iterable['Vertex'], angle: 0., deg: bool = True) -> \ 363 Iterable[Vec3]: 364 """ Rotate `vertices` about to z-axis at to origin (0, 0), faster than a 365 Matrix44 transformation. 366 367 Args: 368 vertices: iterable of vertices 369 angle: rotation angle 370 deg: True if angle in degrees, False if angle in radians 371 372 Returns: yields transformed vertices 373 374 """ 375 if deg: 376 return (Vec3(v).rotate_deg(angle) for v in vertices) 377 else: 378 return (Vec3(v).rotate(angle) for v in vertices) 379 380 381def scale(vertices: Iterable['Vertex'], scaling=(1., 1., 1.)) -> Iterable[Vec3]: 382 """ Scale `vertices` around the origin (0, 0), faster than a Matrix44 383 transformation. 384 385 Args: 386 vertices: iterable of vertices 387 scaling: scale factors as tuple of floats for x-, y- and z-axis 388 389 Returns: yields scaled vertices 390 391 """ 392 sx, sy, sz = scaling 393 for v in vertices: 394 v = Vec3(v) 395 yield Vec3(v.x * sx, v.y * sy, v.z * sz) 396 397 398def close_polygon(vertices: Iterable['Vertex'], 399 rel_tol: float = 1e-9, 400 abs_tol: float = 1e-12) -> List['Vertex']: 401 """ Returns list of vertices, where vertices[0] == vertices[-1]. 402 """ 403 vertices = list(vertices) 404 if not Vec3(vertices[0]).isclose( 405 vertices[-1], rel_tol=rel_tol, abs_tol=abs_tol): 406 vertices.append(vertices[0]) 407 return vertices 408 409 410# 8 corner vertices 411_cube_vertices = [ 412 Vec3(0, 0, 0), 413 Vec3(1, 0, 0), 414 Vec3(1, 1, 0), 415 Vec3(0, 1, 0), 416 Vec3(0, 0, 1), 417 Vec3(1, 0, 1), 418 Vec3(1, 1, 1), 419 Vec3(0, 1, 1), 420] 421 422# 8 corner vertices, 'mass' center in (0, 0, 0) 423_cube0_vertices = [ 424 Vec3(-.5, -.5, -.5), 425 Vec3(+.5, -.5, -.5), 426 Vec3(+.5, +.5, -.5), 427 Vec3(-.5, +.5, -.5), 428 Vec3(-.5, -.5, +.5), 429 Vec3(+.5, -.5, +.5), 430 Vec3(+.5, +.5, +.5), 431 Vec3(-.5, +.5, +.5), 432] 433 434# 6 cube faces 435cube_faces = [ 436 [0, 3, 2, 1], 437 [4, 5, 6, 7], 438 [0, 1, 5, 4], 439 [1, 2, 6, 5], 440 [3, 7, 6, 2], 441 [0, 4, 7, 3], 442] 443 444 445def cube(center: bool = True) -> MeshTransformer: 446 """ Create a `cube <https://en.wikipedia.org/wiki/Cube>`_ as 447 :class:`~ezdxf.render.MeshTransformer` object. 448 449 Args: 450 center: 'mass' center of cube, ``(0, 0, 0)`` if ``True``, else first 451 corner at ``(0, 0, 0)`` 452 453 Returns: :class:`~ezdxf.render.MeshTransformer` 454 455 """ 456 mesh = MeshTransformer() 457 vectices = _cube0_vertices if center else _cube_vertices 458 mesh.add_mesh(vertices=vectices, faces=cube_faces) 459 return mesh 460 461 462def extrude(profile: Iterable['Vertex'], path: Iterable['Vertex'], 463 close=True) -> MeshTransformer: 464 """ Extrude a `profile` polygon along a `path` polyline, vertices of profile 465 should be in counter clockwise order. 466 467 Args: 468 profile: sweeping profile as list of (x, y, z) tuples in counter 469 clockwise order 470 path: extrusion path as list of (x, y, z) tuples 471 close: close profile polygon if ``True`` 472 473 Returns: :class:`~ezdxf.render.MeshTransformer` 474 475 """ 476 477 def add_hull(bottom_profile, top_profile): 478 prev_bottom = bottom_profile[0] 479 prev_top = top_profile[0] 480 for bottom, top in zip(bottom_profile[1:], top_profile[1:]): 481 face = (prev_bottom, bottom, top, 482 prev_top) # counter clock wise: normals outwards 483 mesh.faces.append(face) 484 prev_bottom = bottom 485 prev_top = top 486 487 mesh = MeshVertexMerger() 488 if close: 489 profile = close_polygon(profile) 490 profile = [Vec3(p) for p in profile] 491 path = [Vec3(p) for p in path] 492 start_point = path[0] 493 bottom_indices = mesh.add_vertices(profile) # base profile 494 for target_point in path[1:]: 495 translation_vector = target_point - start_point 496 # profile will just be translated 497 profile = [vec + translation_vector for vec in profile] 498 top_indices = mesh.add_vertices(profile) 499 add_hull(bottom_indices, top_indices) 500 bottom_indices = top_indices 501 start_point = target_point 502 return MeshTransformer.from_builder(mesh) 503 504 505def cylinder(count: int = 16, radius: float = 1., top_radius: float = None, 506 top_center: 'Vertex' = (0, 0, 1), 507 caps=True, ngons=True) -> MeshTransformer: 508 """ Create a `cylinder <https://en.wikipedia.org/wiki/Cylinder>`_ as 509 :class:`~ezdxf.render.MeshTransformer` object, the base center is fixed in 510 the origin (0, 0, 0). 511 512 Args: 513 count: profiles edge count 514 radius: radius for bottom profile 515 top_radius: radius for top profile, if ``None`` top_radius == radius 516 top_center: location vector for the center of the top profile 517 caps: close hull with bottom cap and top cap (as N-gons) 518 ngons: use ngons for caps if ``True`` else subdivide caps into triangles 519 520 Returns: :class:`~ezdxf.render.MeshTransformer` 521 522 """ 523 if top_radius is None: 524 top_radius = radius 525 526 if isclose(top_radius, 0.): # pyramid/cone 527 return cone(count=count, radius=radius, apex=top_center) 528 529 base_profile = list(circle(count, radius, close=True)) 530 top_profile = list( 531 translate(circle(count, top_radius, close=True), top_center)) 532 return from_profiles_linear([base_profile, top_profile], caps=caps, 533 ngons=ngons) 534 535 536def cylinder_2p(count: int = 16, radius: float = 1, base_center=(0, 0, 0), 537 top_center=(0, 0, 1), ) -> MeshTransformer: 538 """ Create a `cylinder <https://en.wikipedia.org/wiki/Cylinder>`_ as 539 :class:`~ezdxf.render.MeshTransformer` object from two points, 540 `base_center` is the center of the base circle and, `top_center` the center 541 of the top circle. 542 543 Args: 544 count: profiles edge count 545 radius: radius for bottom profile 546 base_center: center of base circle 547 top_center: center of top circle 548 549 Returns: :class:`~ezdxf.render.MeshTransformer` 550 551 """ 552 # Copyright (c) 2011 Evan Wallace (http://madebyevan.com/), under the MIT license. 553 # Python port Copyright (c) 2012 Tim Knip (http://www.floorplanner.com), under the MIT license. 554 # Additions by Alex Pletzer (Pennsylvania State University) 555 # Adaptation for ezdxf, Copyright (c) 2020, Manfred Moitzi, MIT License. 556 start = Vec3(base_center) 557 end = Vec3(top_center) 558 radius = float(radius) 559 slices = int(count) 560 ray = end - start 561 562 z_axis = ray.normalize() 563 is_y = (fabs(z_axis.y) > 0.5) 564 x_axis = Vec3(float(is_y), float(not is_y), 0).cross(z_axis).normalize() 565 y_axis = x_axis.cross(z_axis).normalize() 566 mesh = MeshVertexMerger() 567 568 def vertex(stack, angle): 569 out = (x_axis * cos(angle)) + (y_axis * sin(angle)) 570 return start + (ray * stack) + (out * radius) 571 572 dt = pi * 2 / float(slices) 573 for i in range(0, slices): 574 t0 = i * dt 575 i1 = (i + 1) % slices 576 t1 = i1 * dt 577 mesh.add_face([start, vertex(0, t0), vertex(0, t1)]) 578 mesh.add_face( 579 [vertex(0, t1), vertex(0, t0), vertex(1, t0), vertex(1, t1)]) 580 mesh.add_face([end, vertex(1, t1), vertex(1, t0)]) 581 return MeshTransformer.from_builder(mesh) 582 583 584def ngon_to_triangles(face: Iterable['Vertex']) -> Iterable[Sequence[Vec3]]: 585 face = [Vec3(v) for v in face] 586 if face[0].isclose(face[-1]): # closed shape 587 center = Vec3.sum(face[:-1]) / (len(face) - 1) 588 else: 589 center = Vec3.sum(face) / len(face) 590 face.append(face[0]) 591 592 for v1, v2 in zip(face[:-1], face[1:]): 593 yield v1, v2, center 594 595 596def from_profiles_linear(profiles: Iterable[Iterable['Vertex']], close=True, 597 caps=False, ngons=True) -> MeshTransformer: 598 """ Create MESH entity by linear connected `profiles`. 599 600 Args: 601 profiles: list of profiles 602 close: close profile polygon if ``True`` 603 caps: close hull with bottom cap and top cap 604 ngons: use ngons for caps if ``True`` else subdivide caps into triangles 605 606 Returns: :class:`~ezdxf.render.MeshTransformer` 607 608 """ 609 mesh = MeshVertexMerger() 610 profiles = list(profiles) 611 if close: 612 profiles = [close_polygon(p) for p in profiles] 613 if caps: 614 base = reversed(profiles[0]) # for correct outside pointing normals 615 top = profiles[-1] 616 if ngons: 617 mesh.add_face(base) 618 mesh.add_face(top) 619 else: 620 for face in ngon_to_triangles(base): 621 mesh.add_face(face) 622 for face in ngon_to_triangles(top): 623 mesh.add_face(face) 624 625 for profile1, profile2 in zip(profiles, profiles[1:]): 626 prev_v1, prev_v2 = None, None 627 for v1, v2 in zip(profile1, profile2): 628 if prev_v1 is not None: 629 mesh.add_face([prev_v1, v1, v2, prev_v2]) 630 prev_v1 = v1 631 prev_v2 = v2 632 633 return MeshTransformer.from_builder(mesh) 634 635 636def spline_interpolation(vertices: Iterable['Vertex'], degree: int = 3, 637 method: str = 'chord', 638 subdivide: int = 4) -> List[Vec3]: 639 """ B-spline interpolation, vertices are fit points for the spline 640 definition. 641 642 Only method 'uniform', yields vertices at fit points. 643 644 Args: 645 vertices: fit points 646 degree: degree of B-spline 647 method: "uniform", "chord"/"distance", "centripetal"/"sqrt_chord" or 648 "arc" calculation method for parameter t 649 subdivide: count of sub vertices + 1, e.g. 4 creates 3 sub-vertices 650 651 Returns: list of vertices 652 653 """ 654 vertices = list(vertices) 655 spline = global_bspline_interpolation(vertices, degree=degree, 656 method=method) 657 return list(spline.approximate(segments=(len(vertices) - 1) * subdivide)) 658 659 660def spline_interpolated_profiles(profiles: Iterable[Iterable['Vertex']], 661 subdivide: int = 4) -> Iterable[List[Vec3]]: 662 """ Profile interpolation by cubic B-spline interpolation. 663 664 Args: 665 profiles: list of profiles 666 subdivide: count of interpolated profiles + 1, e.g. 4 creates 3 667 sub-profiles between two main profiles (4 face loops) 668 669 Returns: yields profiles as list of vertices 670 671 """ 672 profiles = [list(p) for p in profiles] 673 if len(set(len(p) for p in profiles)) != 1: 674 raise ValueError('All profiles have to have the same vertex count') 675 676 vertex_count = len(profiles[0]) 677 edges = [] # interpolated spline vertices, where profile vertices are fit points 678 for index in range(vertex_count): 679 edge_vertices = [p[index] for p in profiles] 680 edges.append(spline_interpolation(edge_vertices, subdivide=subdivide)) 681 682 profile_count = len(edges[0]) 683 for profile_index in range(profile_count): 684 yield [edge[profile_index] for edge in edges] 685 686 687def from_profiles_spline(profiles: Iterable[Iterable['Vertex']], 688 subdivide: int = 4, close=True, 689 caps=False, ngons=True) -> MeshTransformer: 690 """ Create MESH entity by spline interpolation between given `profiles`. 691 Requires at least 4 profiles. A subdivide value of 4, means, create 4 face 692 loops between two profiles, without interpolation two profiles create one 693 face loop. 694 695 Args: 696 profiles: list of profiles 697 subdivide: count of face loops 698 close: close profile polygon if ``True`` 699 caps: close hull with bottom cap and top cap 700 ngons: use ngons for caps if ``True`` else subdivide caps into triangles 701 702 Returns: :class:`~ezdxf.render.MeshTransformer` 703 704 """ 705 profiles = list(profiles) 706 if len(profiles) > 3: 707 profiles = spline_interpolated_profiles(profiles, subdivide) 708 else: 709 raise ValueError("Spline interpolation requires at least 4 profiles") 710 return from_profiles_linear(profiles, close=close, caps=caps, ngons=ngons) 711 712 713def cone(count: int = 16, radius: float = 1.0, apex: 'Vertex' = (0, 0, 1), 714 caps=True, ngons=True) -> MeshTransformer: 715 """ Create a `cone <https://en.wikipedia.org/wiki/Cone>`_ as 716 :class:`~ezdxf.render.MeshTransformer` object, the base center is fixed in 717 the origin (0, 0, 0). 718 719 Args: 720 count: edge count of basis_vector 721 radius: radius of basis_vector 722 apex: tip of the cone 723 caps: add a bottom face if ``True`` 724 ngons: use ngons for caps if ``True`` else subdivide caps into triangles 725 726 Returns: :class:`~ezdxf.render.MeshTransformer` 727 728 """ 729 mesh = MeshVertexMerger() 730 base_circle = list(circle(count, radius, close=True)) 731 for p1, p2 in zip(base_circle, base_circle[1:]): 732 mesh.add_face([p1, p2, apex]) 733 if caps: 734 base_circle = reversed( 735 base_circle) # for correct outside pointing normals 736 if ngons: 737 mesh.add_face(base_circle) 738 else: 739 for face in ngon_to_triangles(base_circle): 740 mesh.add_face(face) 741 742 return MeshTransformer.from_builder(mesh) 743 744 745def cone_2p(count: int = 16, radius: float = 1.0, base_center=(0, 0, 0), 746 apex=(0, 0, 1)) -> MeshTransformer: 747 """ Create a `cone <https://en.wikipedia.org/wiki/Cone>`_ as 748 :class:`~ezdxf.render.MeshTransformer` object from two points, `base_center` 749 is the center of the base circle and `apex` as the tip of the cone. 750 751 Args: 752 count: edge count of basis_vector 753 radius: radius of basis_vector 754 base_center: center point of base circle 755 apex: tip of the cone 756 757 Returns: :class:`~ezdxf.render.MeshTransformer` 758 759 """ 760 # Copyright (c) 2011 Evan Wallace (http://madebyevan.com/), under the MIT license. 761 # Python port Copyright (c) 2012 Tim Knip (http://www.floorplanner.com), under the MIT license. 762 # Additions by Alex Pletzer (Pennsylvania State University) 763 # Adaptation for ezdxf, Copyright (c) 2020, Manfred Moitzi, MIT License. 764 start = Vec3(base_center) 765 end = Vec3(apex) 766 slices = int(count) 767 ray = end - start 768 z_axis = ray.normalize() 769 is_y = (fabs(z_axis.y) > 0.5) 770 x_axis = Vec3(float(is_y), float(not is_y), 0).cross(z_axis).normalize() 771 y_axis = x_axis.cross(z_axis).normalize() 772 mesh = MeshVertexMerger() 773 774 def vertex(angle) -> Vec3: 775 # radial direction pointing out 776 out = x_axis * cos(angle) + y_axis * sin(angle) 777 return start + out * radius 778 779 dt = pi * 2.0 / slices 780 for i in range(0, slices): 781 t0 = i * dt 782 i1 = (i + 1) % slices 783 t1 = i1 * dt 784 # coordinates and associated normal pointing outwards of the cone's 785 # side 786 p0 = vertex(t0) 787 p1 = vertex(t1) 788 # polygon on the low side (disk sector) 789 mesh.add_face([start, p0, p1]) 790 # polygon extending from the low side to the tip 791 mesh.add_face([p0, end, p1]) 792 793 return MeshTransformer.from_builder(mesh) 794 795 796def rotation_form(count: int, profile: Iterable['Vertex'], 797 angle: float = 2 * pi, 798 axis: 'Vertex' = (1, 0, 0)) -> MeshTransformer: 799 """ Create MESH entity by rotating a `profile` around an `axis`. 800 801 Args: 802 count: count of rotated profiles 803 profile: profile to rotate as list of vertices 804 angle: rotation angle in radians 805 axis: rotation axis 806 807 Returns: :class:`~ezdxf.render.MeshTransformer` 808 809 """ 810 if count < 3: 811 raise ValueError('count >= 2') 812 delta = float(angle) / count 813 m = Matrix44.axis_rotate(Vec3(axis), delta) 814 profile = [Vec3(p) for p in profile] 815 profiles = [profile] 816 for _ in range(int(count)): 817 profile = list(m.transform_vertices(profile)) 818 profiles.append(profile) 819 mesh = from_profiles_linear(profiles, close=False, caps=False) 820 return mesh 821 822 823def sphere(count: int = 16, stacks: int = 8, radius: float = 1, 824 quads=True) -> MeshTransformer: 825 """ Create a `sphere <https://en.wikipedia.org/wiki/Sphere>`_ as 826 :class:`~ezdxf.render.MeshTransformer` object, center is fixed at origin 827 (0, 0, 0). 828 829 Args: 830 count: longitudinal slices 831 stacks: latitude slices 832 radius: radius of sphere 833 quads: use quads for body faces if ``True`` else triangles 834 835 Returns: :class:`~ezdxf.render.MeshTransformer` 836 837 """ 838 radius = float(radius) 839 slices = int(count) 840 stacks_2 = int(stacks) // 2 # stacks from -stack/2 to +stack/2 841 delta_theta = pi * 2.0 / float(slices) 842 delta_phi = pi / float(stacks) 843 mesh = MeshVertexMerger() 844 845 def radius_of_stack(stack: float) -> float: 846 return radius * cos(delta_phi * stack) 847 848 def vertex(slice_: float, r: float, z: float) -> Vec3: 849 actual_theta = delta_theta * slice_ 850 return Vec3(cos(actual_theta) * r, sin(actual_theta) * r, z) 851 852 def cap_triangles(stack, top=False): 853 z = sin(stack * delta_phi) * radius 854 cap_vertex = Vec3(0, 0, radius) if top else Vec3(0, 0, -radius) 855 r1 = radius_of_stack(stack) 856 for slice_ in range(slices): 857 v1 = vertex(slice_, r1, z) 858 v2 = vertex(slice_ + 1, r1, z) 859 if top: 860 mesh.add_face((v1, v2, cap_vertex)) 861 else: 862 mesh.add_face((cap_vertex, v2, v1)) 863 864 # bottom triangle faces 865 cap_triangles(-stacks_2 + 1, top=False) 866 867 # add body faces 868 for actual_stack in range(-stacks_2 + 1, stacks_2 - 1): 869 next_stack = actual_stack + 1 870 r1 = radius_of_stack(actual_stack) 871 r2 = radius_of_stack(next_stack) 872 z1 = sin(delta_phi * actual_stack) * radius 873 z2 = sin(delta_phi * next_stack) * radius 874 for i in range(slices): 875 v1 = vertex(i, r1, z1) 876 v2 = vertex(i + 1, r1, z1) 877 v3 = vertex(i + 1, r2, z2) 878 v4 = vertex(i, r2, z2) 879 if quads: 880 mesh.add_face([v1, v2, v3, v4]) 881 else: 882 center = vertex( 883 i + 0.5, 884 radius_of_stack(actual_stack + 0.5), 885 sin(delta_phi * (actual_stack + 0.5)) * radius, 886 ) 887 mesh.add_face([v1, v2, center]) 888 mesh.add_face([v2, v3, center]) 889 mesh.add_face([v3, v4, center]) 890 mesh.add_face([v4, v1, center]) 891 892 # top triangle faces 893 cap_triangles(stacks_2 - 1, top=True) 894 895 return MeshTransformer.from_builder(mesh) 896