@@ -97,13 +97,13 @@ class Path:
97
97
CLOSEPOLY : 1 }
98
98
99
99
def __init__ (self , vertices , codes = None , _interpolation_steps = 1 ,
100
- closed = False , readonly = False ):
100
+ closed = False , readonly = False , dims = 2 ):
101
101
"""
102
102
Create a new path with the given vertices and codes.
103
103
104
104
Parameters
105
105
----------
106
- vertices : (N, 2 ) array-like
106
+ vertices : (N, dims ) array-like
107
107
The path vertices, as an array, masked array or sequence of pairs.
108
108
Masked values, if any, will be converted to NaNs, which are then
109
109
handled correctly by the Agg PathIterator and other consumers of
@@ -125,9 +125,14 @@ def __init__(self, vertices, codes=None, _interpolation_steps=1,
125
125
readonly : bool, optional
126
126
Makes the path behave in an immutable way and sets the vertices
127
127
and codes as read-only arrays.
128
+ dims : int, optional
128
129
"""
130
+ if dims <= 1 :
131
+ raise ValueError ("Path must be at least 2D" )
132
+ self ._dims = dims
133
+
129
134
vertices = _to_unmasked_float_array (vertices )
130
- _api .check_shape ((None , 2 ), vertices = vertices )
135
+ _api .check_shape ((None , dims ), vertices = vertices )
131
136
132
137
if codes is not None :
133
138
codes = np .asarray (codes , self .code_type )
@@ -178,6 +183,7 @@ def _fast_from_codes_and_verts(cls, verts, codes, internals_from=None):
178
183
pth ._vertices = _to_unmasked_float_array (verts )
179
184
pth ._codes = codes
180
185
pth ._readonly = False
186
+ pth ._dims = pth ._vertices .shape [1 ]
181
187
if internals_from is not None :
182
188
pth ._should_simplify = internals_from ._should_simplify
183
189
pth ._simplify_threshold = internals_from ._simplify_threshold
@@ -210,13 +216,15 @@ def _update_values(self):
210
216
211
217
@property
212
218
def vertices (self ):
213
- """The vertices of the `Path` as an (N, 2 ) array."""
219
+ """The vertices of the `Path` as an (N, dims ) array."""
214
220
return self ._vertices
215
221
216
222
@vertices .setter
217
223
def vertices (self , vertices ):
218
224
if self ._readonly :
219
225
raise AttributeError ("Can't set vertices on a readonly Path" )
226
+ if not _api .check_shape ((None , self ._dims ), vertices = vertices ):
227
+ raise ValueError ("Vertices shape does not match path dimensions" )
220
228
self ._vertices = vertices
221
229
self ._update_values ()
222
230
@@ -239,6 +247,26 @@ def codes(self, codes):
239
247
self ._codes = codes
240
248
self ._update_values ()
241
249
250
+ @property
251
+ def dims (self ):
252
+ """
253
+ The dimensions of vertices in the `Path`.
254
+ """
255
+ return self ._dims
256
+
257
+ @dims .setter
258
+ def dims (self , dims ):
259
+ if dims <= 2 :
260
+ raise ValueError ("Path must be at least 2D" )
261
+
262
+ if dims < self ._dims :
263
+ self ._vertices = self ._vertices [:, :dims ]
264
+ elif dims > self ._dims :
265
+ self ._vertices = np .pad (self ._vertices ,
266
+ ((0 , 0 ), (0 , dims - self ._dims )),
267
+ mode = 'constant' , constant_values = np .nan )
268
+ self ._dims = dims
269
+
242
270
@property
243
271
def simplify_threshold (self ):
244
272
"""
@@ -298,17 +326,17 @@ def make_compound_path_from_polys(cls, XY):
298
326
299
327
Parameters
300
328
----------
301
- XY : (numpolys, numsides, 2 ) array
329
+ XY : (numpolys, numsides, dims ) array
302
330
"""
303
331
# for each poly: 1 for the MOVETO, (numsides-1) for the LINETO, 1 for
304
332
# the CLOSEPOLY; the vert for the closepoly is ignored but we still
305
333
# need it to keep the codes aligned with the vertices
306
- numpolys , numsides , two = XY .shape
307
- if two != 2 :
308
- raise ValueError ("The third dimension of 'XY' must be 2" )
334
+ numpolys , numsides , dims = XY .shape
335
+ if dims < 2 :
336
+ raise ValueError ("The third dimension of 'XY' must be at least 2" )
309
337
stride = numsides + 1
310
338
nverts = numpolys * stride
311
- verts = np .zeros ((nverts , 2 ))
339
+ verts = np .zeros ((nverts , dims ))
312
340
codes = np .full (nverts , cls .LINETO , dtype = cls .code_type )
313
341
codes [0 ::stride ] = cls .MOVETO
314
342
codes [numsides ::stride ] = cls .CLOSEPOLY
@@ -323,6 +351,9 @@ def make_compound_path(cls, *args):
323
351
"""
324
352
if not args :
325
353
return Path (np .empty ([0 , 2 ], dtype = np .float32 ))
354
+ if not all ([path ._dims != args [0 ]._dims for path in args ]):
355
+ raise ValueError ("Paths provided must be the same dimension" )
356
+
326
357
vertices = np .concatenate ([path .vertices for path in args ])
327
358
codes = np .empty (len (vertices ), dtype = cls .code_type )
328
359
i = 0
@@ -338,6 +369,16 @@ def make_compound_path(cls, *args):
338
369
not_stop_mask = codes != cls .STOP # Remove STOPs, as internal STOPs are a bug.
339
370
return cls (vertices [not_stop_mask ], codes [not_stop_mask ])
340
371
372
+ @classmethod
373
+ def _project_to_2d (cls , path , transform = None ):
374
+ if transform is not None :
375
+ path = transform .transform_path (path )
376
+
377
+ if path ._dims > 2 :
378
+ path = Path ._fast_from_codes_and_verts (path .vertices [:, 2 ], path .codes ,
379
+ path )
380
+ return path
381
+
341
382
def __repr__ (self ):
342
383
return f"Path({ self .vertices !r} , { self .codes !r} )"
343
384
@@ -478,6 +519,10 @@ def cleaned(self, transform=None, remove_nans=False, clip=None,
478
519
--------
479
520
Path.iter_segments : for details of the keyword arguments.
480
521
"""
522
+ # Not implemented for non 2D
523
+ if self ._dims != 2 :
524
+ return self
525
+
481
526
vertices , codes = _path .cleanup_path (
482
527
self , transform , remove_nans , clip , snap , stroke_width , simplify ,
483
528
curves , sketch )
@@ -497,7 +542,7 @@ def transformed(self, transform):
497
542
automatically update when the transform changes.
498
543
"""
499
544
return Path (transform .transform (self .vertices ), self .codes ,
500
- self ._interpolation_steps )
545
+ self ._interpolation_steps , dims = transform . output_dims )
501
546
502
547
def contains_point (self , point , transform = None , radius = 0.0 ):
503
548
"""
@@ -540,14 +585,22 @@ def contains_point(self, point, transform=None, radius=0.0):
540
585
"""
541
586
if transform is not None :
542
587
transform = transform .frozen ()
588
+
589
+ # Transform the path, and then toss out the z dimension
590
+ if self .dims > 2 :
591
+ pth = Path ._project_to_2d (self , transform )
592
+ transform = None
593
+
543
594
# `point_in_path` does not handle nonlinear transforms, so we
544
595
# transform the path ourselves. If *transform* is affine, letting
545
596
# `point_in_path` handle the transform avoids allocating an extra
546
597
# buffer.
547
- if transform and not transform .is_affine :
598
+ elif transform and not transform .is_affine :
548
599
self = transform .transform_path (self )
600
+ pth = self
549
601
transform = None
550
- return _path .point_in_path (point [0 ], point [1 ], radius , self , transform )
602
+
603
+ return _path .point_in_path (point [0 ], point [1 ], radius , pth , transform )
551
604
552
605
def contains_points (self , points , transform = None , radius = 0.0 ):
553
606
"""
@@ -590,7 +643,11 @@ def contains_points(self, points, transform=None, radius=0.0):
590
643
"""
591
644
if transform is not None :
592
645
transform = transform .frozen ()
593
- result = _path .points_in_path (points , radius , self , transform )
646
+ pth = self
647
+ if self ._dims > 2 :
648
+ pth = Path ._project_to_2d (self , transform )
649
+ transform = None
650
+ result = _path .points_in_path (points , radius , pth , transform )
594
651
return result .astype ('bool' )
595
652
596
653
def contains_path (self , path , transform = None ):
@@ -602,7 +659,15 @@ def contains_path(self, path, transform=None):
602
659
"""
603
660
if transform is not None :
604
661
transform = transform .frozen ()
605
- return _path .path_in_path (self , None , path , transform )
662
+
663
+ a_pth = Path ._project_to_2d (self , None )
664
+ if path ._dims > 2 :
665
+ b_pth = Path ._project_to_2d (path , transform )
666
+ transform = None
667
+ else :
668
+ b_pth = path
669
+
670
+ return _path .path_in_path (a_pth , None , b_pth , transform )
606
671
607
672
def get_extents (self , transform = None , ** kwargs ):
608
673
"""
@@ -652,7 +717,9 @@ def intersects_path(self, other, filled=True):
652
717
If *filled* is True, then this also returns True if one path completely
653
718
encloses the other (i.e., the paths are treated as filled).
654
719
"""
655
- return _path .path_intersects_path (self , other , filled )
720
+ a = Path ._project_to_2d (self )
721
+ b = Path ._project_to_2d (other )
722
+ return _path .path_intersects_path (a , b , filled )
656
723
657
724
def intersects_bbox (self , bbox , filled = True ):
658
725
"""
@@ -663,8 +730,9 @@ def intersects_bbox(self, bbox, filled=True):
663
730
664
731
The bounding box is always considered filled.
665
732
"""
733
+ pth = Path ._project_to_2d (self )
666
734
return _path .path_intersects_rectangle (
667
- self , bbox .x0 , bbox .y0 , bbox .x1 , bbox .y1 , filled )
735
+ pth , bbox .x0 , bbox .y0 , bbox .x1 , bbox .y1 , filled )
668
736
669
737
def interpolated (self , steps ):
670
738
"""
@@ -683,7 +751,7 @@ def interpolated(self, steps):
683
751
new_codes [0 ::steps ] = codes
684
752
else :
685
753
new_codes = None
686
- return Path (vertices , new_codes )
754
+ return Path (vertices , new_codes , dims = vertices . shape [ 1 ] )
687
755
688
756
def to_polygons (self , transform = None , width = 0 , height = 0 , closed_only = True ):
689
757
"""
0 commit comments