316 lines
9.0 KiB
Python
316 lines
9.0 KiB
Python
|
import numpy as np
|
||
|
|
||
|
from . import arc
|
||
|
from .entities import Line, Arc
|
||
|
from ..geometry import plane_transform
|
||
|
|
||
|
from .. import util
|
||
|
from .. import transformations
|
||
|
|
||
|
|
||
|
def circle_pattern(pattern_radius,
|
||
|
circle_radius,
|
||
|
count,
|
||
|
center=[0.0, 0.0],
|
||
|
angle=None,
|
||
|
**kwargs):
|
||
|
"""
|
||
|
Create a Path2D representing a circle pattern.
|
||
|
|
||
|
Parameters
|
||
|
------------
|
||
|
pattern_radius : float
|
||
|
Radius of circle centers
|
||
|
circle_radius : float
|
||
|
The radius of each circle
|
||
|
count : int
|
||
|
Number of circles in the pattern
|
||
|
center : (2,) float
|
||
|
Center of pattern
|
||
|
angle : float
|
||
|
If defined pattern will span this angle
|
||
|
If None, pattern will be evenly spaced
|
||
|
|
||
|
Returns
|
||
|
-------------
|
||
|
pattern : trimesh.path.Path2D
|
||
|
Path containing circular pattern
|
||
|
"""
|
||
|
from .path import Path2D
|
||
|
|
||
|
if angle is None:
|
||
|
angles = np.linspace(0.0, np.pi * 2.0, count + 1)[:-1]
|
||
|
elif isinstance(angle, float) or isinstance(angle, int):
|
||
|
angles = np.linspace(0.0, angle, count)
|
||
|
else:
|
||
|
raise ValueError('angle must be float or int!')
|
||
|
|
||
|
# centers of circles
|
||
|
centers = np.column_stack((
|
||
|
np.cos(angles), np.sin(angles))) * pattern_radius
|
||
|
|
||
|
vert = []
|
||
|
ents = []
|
||
|
for circle_center in centers:
|
||
|
# (3,3) center points of arc
|
||
|
three = arc.to_threepoint(angles=[0, np.pi],
|
||
|
center=circle_center,
|
||
|
radius=circle_radius)
|
||
|
# add a single circle entity
|
||
|
ents.append(
|
||
|
Arc(
|
||
|
points=np.arange(3) + len(vert),
|
||
|
closed=True))
|
||
|
# keep flat array by extend instead of append
|
||
|
vert.extend(three)
|
||
|
|
||
|
# translate vertices to pattern center
|
||
|
vert = np.array(vert) + center
|
||
|
pattern = Path2D(entities=ents,
|
||
|
vertices=vert,
|
||
|
**kwargs)
|
||
|
return pattern
|
||
|
|
||
|
|
||
|
def circle(radius, center=None, **kwargs):
|
||
|
"""
|
||
|
Create a Path2D containing circle with the specified
|
||
|
radius.
|
||
|
|
||
|
Parameters
|
||
|
--------------
|
||
|
radius : float
|
||
|
The radius of the circle
|
||
|
center : None or (2,) float
|
||
|
Center of the circle, origin by default
|
||
|
** kwargs : dict
|
||
|
Passed to trimesh.path.Path2D constructor
|
||
|
|
||
|
Returns
|
||
|
-------------
|
||
|
circle : Path2D
|
||
|
Path containing specified circle
|
||
|
"""
|
||
|
from .path import Path2D
|
||
|
|
||
|
if center is None:
|
||
|
center = [0.0, 0.0]
|
||
|
else:
|
||
|
center = np.asanyarray(center, dtype=np.float64)
|
||
|
# make sure radius is a float
|
||
|
radius = float(radius)
|
||
|
|
||
|
# (3, 2) float, points on arc
|
||
|
three = arc.to_threepoint(angles=[0, np.pi],
|
||
|
center=center,
|
||
|
radius=radius)
|
||
|
# generate the path object
|
||
|
result = Path2D(
|
||
|
entities=[Arc(points=np.arange(3), closed=True)],
|
||
|
vertices=three,
|
||
|
**kwargs)
|
||
|
|
||
|
return result
|
||
|
|
||
|
|
||
|
def rectangle(bounds, **kwargs):
|
||
|
"""
|
||
|
Create a Path2D containing a single or multiple rectangles
|
||
|
with the specified bounds.
|
||
|
|
||
|
Parameters
|
||
|
--------------
|
||
|
bounds : (2, 2) float, or (m, 2, 2) float
|
||
|
Minimum XY, Maximum XY
|
||
|
|
||
|
Returns
|
||
|
-------------
|
||
|
rect : Path2D
|
||
|
Path containing specified rectangles
|
||
|
"""
|
||
|
from .path import Path2D
|
||
|
|
||
|
# data should be float
|
||
|
bounds = np.asanyarray(bounds, dtype=np.float64)
|
||
|
|
||
|
# bounds are extents, re- shape to origin- centered rectangle
|
||
|
if bounds.shape == (2,):
|
||
|
half = np.abs(bounds) / 2.0
|
||
|
bounds = np.array([-half, half])
|
||
|
|
||
|
# should have one bounds or multiple bounds
|
||
|
if not (util.is_shape(bounds, (2, 2)) or
|
||
|
util.is_shape(bounds, (-1, 2, 2))):
|
||
|
raise ValueError('bounds must be (m, 2, 2) or (2, 2)')
|
||
|
|
||
|
# hold Line objects
|
||
|
lines = []
|
||
|
# hold (n, 2) cartesian points
|
||
|
vertices = []
|
||
|
|
||
|
# loop through each rectangle
|
||
|
for lower, upper in bounds.reshape((-1, 2, 2)):
|
||
|
lines.append(Line((np.arange(5) % 4) + len(vertices)))
|
||
|
vertices.extend([lower,
|
||
|
[upper[0], lower[1]],
|
||
|
upper,
|
||
|
[lower[0], upper[1]]])
|
||
|
|
||
|
# create the Path2D with specified rectangles
|
||
|
rect = Path2D(entities=lines,
|
||
|
vertices=vertices,
|
||
|
**kwargs)
|
||
|
|
||
|
return rect
|
||
|
|
||
|
|
||
|
def box_outline(extents=None, transform=None, **kwargs):
|
||
|
"""
|
||
|
Return a cuboid.
|
||
|
|
||
|
Parameters
|
||
|
------------
|
||
|
extents : float, or (3,) float
|
||
|
Edge lengths
|
||
|
transform: (4, 4) float
|
||
|
Transformation matrix
|
||
|
**kwargs:
|
||
|
passed to Trimesh to create box
|
||
|
|
||
|
Returns
|
||
|
------------
|
||
|
geometry : trimesh.Path3D
|
||
|
Path outline of a cuboid geometry
|
||
|
"""
|
||
|
from .exchange.load import load_path
|
||
|
|
||
|
# create vertices for the box
|
||
|
vertices = [0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1,
|
||
|
1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1]
|
||
|
vertices = np.array(vertices,
|
||
|
order='C',
|
||
|
dtype=np.float64).reshape((-1, 3))
|
||
|
vertices -= 0.5
|
||
|
|
||
|
# resize the vertices based on passed size
|
||
|
if extents is not None:
|
||
|
extents = np.asanyarray(extents, dtype=np.float64)
|
||
|
if extents.shape != (3,):
|
||
|
raise ValueError('Extents must be (3,)!')
|
||
|
vertices *= extents
|
||
|
|
||
|
# apply transform if passed
|
||
|
if transform is not None:
|
||
|
vertices = transformations.transform_points(
|
||
|
vertices, transform)
|
||
|
|
||
|
# vertex indices
|
||
|
indices = [0, 1, 3, 2, 0, 4, 5, 7, 6, 4, 0, 2, 6, 7, 3, 1, 5]
|
||
|
outline = load_path(vertices[indices])
|
||
|
|
||
|
return outline
|
||
|
|
||
|
|
||
|
def grid(side,
|
||
|
count=5,
|
||
|
transform=None,
|
||
|
plane_origin=None,
|
||
|
plane_normal=None,
|
||
|
include_circle=True,
|
||
|
sections_circle=32):
|
||
|
"""
|
||
|
Create a Path3D for a grid visualization of a plane.
|
||
|
|
||
|
Parameters
|
||
|
-----------
|
||
|
side : float
|
||
|
Length of half of a grid side
|
||
|
count : int
|
||
|
Number of grid lines per grid half
|
||
|
transform : None or (4, 4) float
|
||
|
Transformation matrix to move grid location.
|
||
|
Takes precedence over plane_origin if both are passed.
|
||
|
plane_origin : None or (3,) float
|
||
|
Plane origin
|
||
|
plane_normal : None or (3,) float
|
||
|
Unit normal vector
|
||
|
include_circle : bool
|
||
|
Include a circular pattern inside the grid
|
||
|
sections_circle : int
|
||
|
How many sections should the smallest circle have
|
||
|
|
||
|
Returns
|
||
|
----------
|
||
|
grid : trimesh.path.Path3D
|
||
|
Path containing grid plane visualization
|
||
|
"""
|
||
|
from .path import Path3D
|
||
|
|
||
|
# change full side length to half-side
|
||
|
side = float(side)
|
||
|
# make sure count is an integer
|
||
|
count = int(count)
|
||
|
# get a spaced sequence of radius
|
||
|
radii = np.linspace(0.0, side, count + 1)[1:]
|
||
|
# what's the maximum radius
|
||
|
rmax = radii[-1]
|
||
|
|
||
|
# keep a count of the current vertex count
|
||
|
current = 0
|
||
|
# collect vertices and entities
|
||
|
vertices = []
|
||
|
entities = []
|
||
|
for r in radii:
|
||
|
if include_circle:
|
||
|
# scale the section count by radius
|
||
|
circle_res = int((r / radii[0]) * sections_circle)
|
||
|
# generate a circule pattern
|
||
|
theta = np.linspace(0.0, np.pi * 2, circle_res)
|
||
|
circle = np.column_stack((np.cos(theta),
|
||
|
np.sin(theta))) * r
|
||
|
# append the circle pattern
|
||
|
vertices.append(circle)
|
||
|
entities.append(Line(
|
||
|
points=np.arange(len(circle)) + current))
|
||
|
# keep the vertex count correct
|
||
|
current += len(circle)
|
||
|
# generate a series of grid lines
|
||
|
vertices.append([[-rmax, r],
|
||
|
[rmax, r],
|
||
|
[-rmax, -r],
|
||
|
[rmax, -r],
|
||
|
[r, -rmax],
|
||
|
[r, rmax],
|
||
|
[-r, -rmax],
|
||
|
[-r, rmax]])
|
||
|
# append an entity per grid line
|
||
|
for i in [0, 2, 4, 6]:
|
||
|
entities.append(Line(
|
||
|
points=np.arange(2) + current + i))
|
||
|
current += len(vertices[-1])
|
||
|
|
||
|
# add the middle lines which were skipped
|
||
|
vertices.append([[0, rmax],
|
||
|
[0, -rmax],
|
||
|
[-rmax, 0],
|
||
|
[rmax, 0]])
|
||
|
entities.append(Line(points=np.arange(2) + current))
|
||
|
entities.append(Line(points=np.arange(2) + current + 2))
|
||
|
# stack vertices into clean (n, 3) float
|
||
|
vertices = np.vstack(vertices)
|
||
|
|
||
|
# if plane was passed instead of transform create the matrix here
|
||
|
if (transform is None and plane_origin is not None and plane_normal is not None):
|
||
|
transform = np.linalg.inv(plane_transform(
|
||
|
origin=plane_origin, normal=plane_normal))
|
||
|
|
||
|
# stack vertices to 3D
|
||
|
vertices = np.column_stack((vertices, np.zeros(len(vertices))))
|
||
|
# apply transform if passed
|
||
|
if transform is not None:
|
||
|
vertices = transformations.transform_points(
|
||
|
vertices, matrix=transform)
|
||
|
# combine result into a Path3D object
|
||
|
grid_path = Path3D(entities=entities, vertices=vertices)
|
||
|
return grid_path
|