-
-
Notifications
You must be signed in to change notification settings - Fork 363
/
trimesh.py
223 lines (185 loc) · 8.2 KB
/
trimesh.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
from __future__ import absolute_import, division
import numpy as np
from toolz import memoize
from datashader.glyphs.points import _PointLike
from datashader.utils import isreal, ngjit
class _PolygonLike(_PointLike):
"""_PointLike class, with methods overridden for vertex-delimited shapes.
Key differences from _PointLike:
- added self.z as a list, representing vertex weights
- constructor accepts additional kwargs:
* weight_type (bool): Whether the weights are on vertices (True) or on the shapes (False)
* interp (bool): Whether to interpolate (True), or to have one color per shape (False)
"""
def __init__(self, x, y, z=None, weight_type=True, interp=True):
super(_PolygonLike, self).__init__(x, y)
if z is None:
self.z = []
else:
self.z = z
self.interpolate = interp
self.weight_type = weight_type
@property
def ndims(self):
return None
@property
def inputs(self):
return (tuple([self.x, self.y] + list(self.z)) +
(self.weight_type, self.interpolate))
def validate(self, in_dshape):
for col in [self.x, self.y] + list(self.z):
if not isreal(in_dshape.measure[str(col)]):
raise ValueError('{} must be real'.format(col))
def required_columns(self):
return [self.x, self.y] + list(self.z)
def compute_x_bounds(self, df):
xs = df[self.x].values
bounds = self._compute_bounds(xs.reshape(np.prod(xs.shape)))
return self.maybe_expand_bounds(bounds)
def compute_y_bounds(self, df):
ys = df[self.y].values
bounds = self._compute_bounds(ys.reshape(np.prod(ys.shape)))
return self.maybe_expand_bounds(bounds)
class Triangles(_PolygonLike):
"""An unstructured mesh of triangles, with vertices defined by ``xs`` and ``ys``.
Parameters
----------
xs, ys, zs : list of str
Column names of x, y, and (optional) z coordinates of each vertex.
"""
@memoize
def _build_extend(self, x_mapper, y_mapper, info, append):
draw_triangle, draw_triangle_interp = _build_draw_triangle(append)
map_onto_pixel = _build_map_onto_pixel_for_triangle(x_mapper, y_mapper)
extend_triangles = _build_extend_triangles(draw_triangle, draw_triangle_interp, map_onto_pixel)
weight_type = self.weight_type
interpolate = self.interpolate
def extend(aggs, df, vt, bounds, plot_start=True):
cols = info(df)
assert cols, 'There must be at least one column on which to aggregate'
# mapped to pixels, then may be clipped
extend_triangles(vt, bounds, df.values, weight_type, interpolate, aggs, cols)
return extend
def _build_draw_triangle(append):
"""Specialize a triangle plotting kernel for a given append/axis combination"""
@ngjit
def edge_func(ax, ay, bx, by, cx, cy):
return (cx - ax) * (by - ay) - (cy - ay) * (bx - ax)
@ngjit
def draw_triangle_interp(verts, bbox, biases, aggs, weights):
"""Same as `draw_triangle()`, but with weights interpolated from vertex
values.
"""
minx, maxx, miny, maxy = bbox
w0, w1, w2 = weights
if minx == maxx and miny == maxy:
# Subpixel case; area == 0
append(minx, miny, *(aggs + ((w0 + w1 + w2) / 3,)))
else:
(ax, ay), (bx, by), (cx, cy) = verts
bias0, bias1, bias2 = biases
area = edge_func(ax, ay, bx, by, cx, cy)
for j in range(miny, maxy+1):
for i in range(minx, maxx+1):
g2 = edge_func(ax, ay, bx, by, i, j)
g0 = edge_func(bx, by, cx, cy, i, j)
g1 = edge_func(cx, cy, ax, ay, i, j)
if ((g2 + bias0) | (g0 + bias1) | (g1 + bias2)) >= 0:
interp_res = (g0 * w0 + g1 * w1 + g2 * w2) / area
append(i, j, *(aggs + (interp_res,)))
@ngjit
def draw_triangle(verts, bbox, biases, aggs, val):
"""Draw a triangle on a grid.
Plots a triangle with integer coordinates onto a pixel grid,
clipping to the bounds. The vertices are assumed to have
already been scaled and transformed.
"""
minx, maxx, miny, maxy = bbox
if minx == maxx and miny == maxy:
# Subpixel case; area == 0
append(minx, miny, *(aggs + (val,)))
else:
(ax, ay), (bx, by), (cx, cy) = verts
bias0, bias1, bias2 = biases
for j in range(miny, maxy+1):
for i in range(minx, maxx+1):
if ((edge_func(ax, ay, bx, by, i, j) + bias0) >= 0 and
(edge_func(bx, by, cx, cy, i, j) + bias1) >= 0 and
(edge_func(cx, cy, ax, ay, i, j) + bias2) >= 0):
append(i, j, *(aggs + (val,)))
return draw_triangle, draw_triangle_interp
def _build_extend_triangles(draw_triangle, draw_triangle_interp, map_onto_pixel):
@ngjit
def extend_triangles(vt, bounds, verts, weight_type, interpolate, aggs, cols):
"""Aggregate along an array of triangles formed by arrays of CW
vertices. Each row corresponds to a single triangle definition.
`weight_type == True` means "weights are on vertices"
"""
xmin, xmax, ymin, ymax = bounds
cmax_x, cmax_y = max(xmin, xmax), max(ymin, ymax)
cmin_x, cmin_y = min(xmin, xmax), min(ymin, ymax)
vmax_x, vmax_y = map_onto_pixel(vt, bounds, cmax_x, cmax_y)
vmin_x, vmin_y = map_onto_pixel(vt, bounds, cmin_x, cmin_y)
col = cols[0] # Only aggregate over one column, for now
n_tris = verts.shape[0]
for n in range(0, n_tris, 3):
a = verts[n]
b = verts[n+1]
c = verts[n+2]
axn, ayn = a[0], a[1]
bxn, byn = b[0], b[1]
cxn, cyn = c[0], c[1]
col0, col1, col2 = col[n], col[n+1], col[n+2]
# Map triangle vertices onto pixels
ax, ay = map_onto_pixel(vt, bounds, axn, ayn)
bx, by = map_onto_pixel(vt, bounds, bxn, byn)
cx, cy = map_onto_pixel(vt, bounds, cxn, cyn)
# Get bounding box
minx = min(ax, bx, cx)
maxx = max(ax, bx, cx)
miny = min(ay, by, cy)
maxy = max(ay, by, cy)
# Skip any further processing of triangles outside of viewing area
if (minx >= vmax_x or
maxx < vmin_x or
miny >= vmax_y or
maxy < vmin_y):
continue
# Clip bbox to viewing area
minx = max(minx, vmin_x)
maxx = min(maxx, vmax_x)
miny = max(miny, vmin_y)
maxy = min(maxy, vmax_y)
# Prevent double-drawing edges.
# https://msdn.microsoft.com/en-us/library/windows/desktop/bb147314(v=vs.85).aspx
bias0, bias1, bias2 = -1, -1, -1
if ay < by or (by == ay and ax < bx):
bias0 = 0
if by < cy or (cy == by and bx < cx):
bias1 = 0
if cy < ay or (ay == cy and cx < ax):
bias2 = 0
bbox = minx, maxx, miny, maxy
biases = bias0, bias1, bias2
mapped_verts = (ax, ay), (bx, by), (cx, cy)
# draw triangles (will be clipped where outside bounds)
if interpolate:
weights = col0, col1, col2
draw_triangle_interp(mapped_verts, bbox, biases, aggs, weights)
else:
val = (col[n] + col[n+1] + col[n+2]) / 3
draw_triangle(mapped_verts, bbox, biases, aggs, val)
return extend_triangles
def _build_map_onto_pixel_for_triangle(x_mapper, y_mapper):
@ngjit
def map_onto_pixel(vt, bounds, x, y):
"""Map points onto pixel grid.
Points falling on upper bound are mapped into previous bin.
"""
sx, tx, sy, ty = vt
xmax, ymax = bounds[1], bounds[3]
xx = int(x_mapper(x) * sx + tx)
yy = int(y_mapper(y) * sy + ty)
return (xx - 1 if x == xmax else xx,
yy - 1 if y == ymax else yy)
return map_onto_pixel