@@ -223,3 +223,233 @@ def get_path(hatchpattern, density=6):
223
223
cursor += pattern .num_vertices
224
224
225
225
return Path (vertices , codes )
226
+
227
+
228
+ attrs = {
229
+ "scale" : 6 ,
230
+ "weight" : 1.0 ,
231
+ "angle" : 0.0 ,
232
+ "random_rotation" : False ,
233
+ "random_placement" : False ,
234
+ "x_stagger" : 0.5 ,
235
+ "y_stagger" : 0.0 ,
236
+ "filled" : True ,
237
+ }
238
+
239
+
240
+ class HatchStyle :
241
+ def __init__ (self , hatchpattern , ** kwargs ):
242
+ self .hatchpattern = hatchpattern
243
+ self .kwargs = {
244
+ attr : kwargs .get (attr , default ) for attr , default in attrs .items ()
245
+ }
246
+
247
+ def rotate_vertices (self , vertices , angle = None , scale_correction = True ):
248
+ vertices = vertices .copy ()
249
+
250
+ if angle is None :
251
+ angle = self .kwargs ["angle" ]
252
+ angle_rad = np .deg2rad (angle )
253
+
254
+ center = np .mean (vertices , axis = 0 )
255
+ vertices -= center
256
+
257
+ if scale_correction :
258
+ scaling = abs (np .sin (angle_rad )) + abs (np .cos (angle_rad ))
259
+ vertices *= scaling
260
+
261
+ rotation_matrix = np .array (
262
+ [
263
+ [np .cos (angle_rad ), - np .sin (angle_rad )],
264
+ [np .sin (angle_rad ), np .cos (angle_rad )],
265
+ ]
266
+ )
267
+ vertices = np .dot (vertices , rotation_matrix )
268
+ vertices += center
269
+ return vertices
270
+
271
+ def get_vertices_and_codes (self , hatch_buffer_scale = 1.0 ):
272
+ self .hatch_buffer_scale = hatch_buffer_scale
273
+ vertices , codes = np .empty ((0 , 2 )), np .empty (0 , Path .code_type )
274
+
275
+ if self .hatchpattern in hatchpatterns :
276
+ # This is for line hatches
277
+ for func in np .atleast_1d (hatchpatterns [self .hatchpattern ]):
278
+ vertices_part , codes_part = func (self )
279
+ vertices_part = self .rotate_vertices (vertices_part )
280
+
281
+ vertices = np .concatenate ((vertices , vertices_part ))
282
+ codes = np .concatenate ((codes , codes_part ))
283
+ else :
284
+ # This is for marker hatches
285
+ if self .hatchpattern not in MarkerHatchStyle .marker_paths :
286
+ raise ValueError (f"Unknown hatch pattern: { self .hatchpattern } " )
287
+ func = MarkerHatchStyle .marker_pattern
288
+ vertices_part , codes_part = func (self )
289
+
290
+ vertices = np .concatenate ((vertices , vertices_part ))
291
+ codes = np .concatenate ((codes , codes_part ))
292
+
293
+ return vertices , codes
294
+
295
+
296
+ class MarkerHatchStyle (HatchStyle ):
297
+ marker_paths = {
298
+ "o" : Path .unit_circle ,
299
+ "O" : Path .unit_circle ,
300
+ "*" : (Path .unit_regular_star , 5 ), # TODO: is there a better way to do this?
301
+ }
302
+
303
+ # TODO: saner defaults or no?
304
+ marker_sizes = {
305
+ "o" : 0.2 ,
306
+ "O" : 0.35 ,
307
+ "*" : 1.0 / 3.0 ,
308
+ }
309
+
310
+ def _get_marker_path (marker ):
311
+ func = np .atleast_1d (MarkerHatchStyle .marker_paths [marker ])
312
+ path = func [0 ](* func [1 :])
313
+ size = MarkerHatchStyle .marker_sizes .get (marker , attrs ["weight" ])
314
+
315
+ return Path (
316
+ vertices = path .vertices * size ,
317
+ codes = path .codes ,
318
+ )
319
+
320
+ def marker_pattern (hatchstyle ):
321
+ size = hatchstyle .kwargs ["weight" ]
322
+ num_rows = round (hatchstyle .kwargs ["scale" ] * hatchstyle .hatch_buffer_scale )
323
+ path = MarkerHatchStyle ._get_marker_path (hatchstyle .hatchpattern )
324
+ marker_vertices = hatchstyle .rotate_vertices (
325
+ path .vertices , scale_correction = False
326
+ )
327
+ marker_codes = path .codes
328
+
329
+ offset = 1.0 / num_rows
330
+ marker_vertices = marker_vertices * offset * size
331
+ x_stagger = hatchstyle .kwargs ["x_stagger" ] * offset
332
+ y_stagger = hatchstyle .kwargs ["y_stagger" ] * offset
333
+
334
+ if not hatchstyle .kwargs ["filled" ]:
335
+ marker_vertices = np .concatenate (
336
+ [marker_vertices , marker_vertices [::- 1 ] * 0.9 ]
337
+ )
338
+ marker_codes = np .concatenate ([marker_codes , marker_codes ])
339
+
340
+ vertices = np .empty ((0 , 2 ))
341
+ codes = np .empty (0 , Path .code_type )
342
+ for row in range (num_rows + 1 ):
343
+ row_pos = row * offset
344
+ if row % 2 == 0 :
345
+ cols = np .linspace (0 , 1 , num_rows + 1 )
346
+ else :
347
+ cols = np .linspace (x_stagger , 1 + x_stagger , num_rows + 1 )
348
+
349
+ for i , col_pos in enumerate (cols ):
350
+ vertices_part = marker_vertices + [col_pos , row_pos ]
351
+ if i % 2 == 1 :
352
+ vertices_part += [0 , y_stagger ]
353
+
354
+ if hatchstyle .kwargs ["random_rotation" ]:
355
+ vertices_part = hatchstyle .rotate_vertices (
356
+ vertices_part , np .random .uniform (0 , 360 ), scale_correction = False
357
+ )
358
+
359
+ if hatchstyle .kwargs ["random_placement" ]:
360
+ vertices_part += np .random .uniform (- offset / 4 , offset / 4 , 2 )
361
+
362
+ vertices = np .concatenate ((vertices , vertices_part ))
363
+ codes = np .concatenate ((codes , marker_codes ))
364
+
365
+ return vertices , codes
366
+
367
+
368
+ class LineHatchStyle (HatchStyle ):
369
+ def horizontal (hatchstyle ):
370
+ num_lines = round (hatchstyle .kwargs ["scale" ] * hatchstyle .hatch_buffer_scale )
371
+ if num_lines :
372
+ num_vertices = num_lines * 2
373
+ else :
374
+ num_vertices = 0
375
+
376
+ vertices = np .empty ((num_vertices , 2 ))
377
+ codes = np .empty (num_vertices , Path .code_type )
378
+ steps , stepsize = np .linspace (0.0 , 1.0 , num_lines , False , retstep = True )
379
+ steps += stepsize / 2.0
380
+ vertices [0 ::2 , 0 ] = 0.0
381
+ vertices [0 ::2 , 1 ] = steps
382
+ vertices [1 ::2 , 0 ] = 1.0
383
+ vertices [1 ::2 , 1 ] = steps
384
+ codes [0 ::2 ] = Path .MOVETO
385
+ codes [1 ::2 ] = Path .LINETO
386
+
387
+ return vertices , codes
388
+
389
+ def vertical (hatchstyle ):
390
+ num_lines = round (hatchstyle .kwargs ["scale" ] * hatchstyle .hatch_buffer_scale )
391
+ if num_lines :
392
+ num_vertices = num_lines * 2
393
+ else :
394
+ num_vertices = 0
395
+
396
+ vertices = np .empty ((num_vertices , 2 ))
397
+ codes = np .empty (num_vertices , Path .code_type )
398
+ steps , stepsize = np .linspace (0.0 , 1.0 , num_lines , False , retstep = True )
399
+ steps += stepsize / 2.0
400
+ vertices [0 ::2 , 0 ] = steps
401
+ vertices [0 ::2 , 1 ] = 0.0
402
+ vertices [1 ::2 , 0 ] = steps
403
+ vertices [1 ::2 , 1 ] = 1.0
404
+ codes [0 ::2 ] = Path .MOVETO
405
+ codes [1 ::2 ] = Path .LINETO
406
+
407
+ return vertices , codes
408
+
409
+ def north_east (hatchstyle ):
410
+ num_lines = round (hatchstyle .kwargs ["scale" ] * hatchstyle .hatch_buffer_scale )
411
+ if num_lines :
412
+ num_vertices = (num_lines + 1 ) * 2
413
+ else :
414
+ num_vertices = 0
415
+
416
+ vertices = np .empty ((num_vertices , 2 ))
417
+ codes = np .empty (num_vertices , Path .code_type )
418
+ steps = np .linspace (- 0.5 , 0.5 , num_lines + 1 )
419
+ vertices [0 ::2 , 0 ] = 0.0 + steps
420
+ vertices [0 ::2 , 1 ] = 0.0 - steps
421
+ vertices [1 ::2 , 0 ] = 1.0 + steps
422
+ vertices [1 ::2 , 1 ] = 1.0 - steps
423
+ codes [0 ::2 ] = Path .MOVETO
424
+ codes [1 ::2 ] = Path .LINETO
425
+
426
+ return vertices , codes
427
+
428
+ def south_east (hatchstyle ):
429
+ num_lines = round (hatchstyle .kwargs ["scale" ] * hatchstyle .hatch_buffer_scale )
430
+ if num_lines :
431
+ num_vertices = (num_lines + 1 ) * 2
432
+ else :
433
+ num_vertices = 0
434
+
435
+ vertices = np .empty ((num_vertices , 2 ))
436
+ codes = np .empty (num_vertices , Path .code_type )
437
+ steps = np .linspace (- 0.5 , 0.5 , num_lines + 1 )
438
+ vertices [0 ::2 , 0 ] = 0.0 + steps
439
+ vertices [0 ::2 , 1 ] = 1.0 + steps
440
+ vertices [1 ::2 , 0 ] = 1.0 + steps
441
+ vertices [1 ::2 , 1 ] = 0.0 + steps
442
+ codes [0 ::2 ] = Path .MOVETO
443
+ codes [1 ::2 ] = Path .LINETO
444
+
445
+ return vertices , codes
446
+
447
+
448
+ hatchpatterns = {
449
+ "-" : LineHatchStyle .horizontal ,
450
+ "|" : LineHatchStyle .vertical ,
451
+ "/" : LineHatchStyle .north_east ,
452
+ "\\ " : LineHatchStyle .south_east ,
453
+ "+" : (LineHatchStyle .horizontal , LineHatchStyle .vertical ),
454
+ "x" : (LineHatchStyle .north_east , LineHatchStyle .south_east ),
455
+ }
0 commit comments