drawlib 0.2.3.dev2__py3-none-any.whl → 0.2.3.dev3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of drawlib might be problematic. Click here for more details.

drawlib/__init__.py CHANGED
@@ -14,7 +14,7 @@ import sys
14
14
  from typing import Final, List
15
15
 
16
16
  # please update here when you release new version
17
- LIB_VERSION = "0.2.3.dev2"
17
+ LIB_VERSION = "0.2.3.dev3"
18
18
 
19
19
  # please list active main committers (1+ commits per month)
20
20
  AUTHORS: Final[List[str]] = [
drawlib/v0_2/apis.py CHANGED
@@ -54,6 +54,8 @@ from drawlib.v0_2.private.core_canvas.canvas import (
54
54
  arc,
55
55
  # shape original
56
56
  arrow,
57
+ arrow_arc,
58
+ arrows,
57
59
  chevron,
58
60
  circle,
59
61
  # base
@@ -66,6 +68,7 @@ from drawlib.v0_2.private.core_canvas.canvas import (
66
68
  image,
67
69
  # line
68
70
  line,
71
+ line_arc,
69
72
  line_bezier1,
70
73
  line_bezier2,
71
74
  line_curved,
@@ -883,6 +883,7 @@ class ShapeTextStyle:
883
883
  angle: Optional[float] = None
884
884
  flip: Optional[bool] = None
885
885
  xy_shift: Optional[Tuple[float, float]] = None
886
+ xy_abs_shift: Optional[Tuple[float, float]] = None
886
887
 
887
888
  @error_handler
888
889
  def copy(self) -> ShapeTextStyle:
@@ -1107,7 +1108,7 @@ class ShapeTextStyle:
1107
1108
 
1108
1109
  @xy_shift.setter
1109
1110
  @error_handler
1110
- def xy_shift(self, value: Optional[float]) -> None:
1111
+ def xy_shift(self, value: Optional[Tuple[float, float]]) -> None:
1111
1112
  """Setter of xy_shift"""
1112
1113
  if isinstance(value, property) or value is None:
1113
1114
  self._xy_shift = None
@@ -1116,6 +1117,23 @@ class ShapeTextStyle:
1116
1117
  coordinate_validator.validate_xy("ShapeTextStyle.xy_shift", value)
1117
1118
  self._xy_shift = value
1118
1119
 
1120
+ @property # type: ignore[no-redef]
1121
+ @error_handler
1122
+ def xy_abs_shift(self) -> Optional[Tuple[float, float]]:
1123
+ """Getter of xy_abs_shift"""
1124
+ return self._xy_abs_shift
1125
+
1126
+ @xy_abs_shift.setter
1127
+ @error_handler
1128
+ def xy_abs_shift(self, value: Optional[Tuple[float, float]]) -> None:
1129
+ """Setter of xy_abs_shift"""
1130
+ if isinstance(value, property) or value is None:
1131
+ self._xy_abs_shift = None
1132
+ return
1133
+
1134
+ coordinate_validator.validate_xy("ShapeTextStyle.xy_abs_shift", value)
1135
+ self._xy_abs_shift = value
1136
+
1119
1137
 
1120
1138
  @dataclasses.dataclass
1121
1139
  class TextStyle:
@@ -498,6 +498,7 @@ class ShapeUtil:
498
498
  text_angle = (text_angle + 180) % 360
499
499
 
500
500
  x, y = xy
501
+ # shift
501
502
  if style.xy_shift is not None:
502
503
  x_shift, y_shift = style.xy_shift
503
504
  if shape_angle == 0:
@@ -510,6 +511,11 @@ class ShapeUtil:
510
511
  x += rotated_x_shift
511
512
  y += rotated_y_shift
512
513
 
514
+ # absolute shift
515
+ if style.xy_abs_shift is not None:
516
+ x += style.xy_abs_shift[0]
517
+ y += style.xy_abs_shift[1]
518
+
513
519
  # only check color. ignore alignment
514
520
  options = TextUtil.get_text_options(style)
515
521
  if "horizontalalignment" in options:
@@ -266,6 +266,7 @@ line = canvas.line
266
266
  line_curved = canvas.line_curved
267
267
  line_bezier1 = canvas.line_bezier1
268
268
  line_bezier2 = canvas.line_bezier2
269
+ line_arc = canvas.line_arc
269
270
  lines = canvas.lines
270
271
  lines_curved = canvas.lines_curved
271
272
  lines_bezier = canvas.lines_bezier
@@ -276,6 +277,8 @@ trapezoid = canvas.trapezoid
276
277
  parallelogram = canvas.parallelogram
277
278
  triangle = canvas.triangle
278
279
  arrow = canvas.arrow
280
+ arrows = canvas.arrows
281
+ arrow_arc = canvas.arrow_arc
279
282
  star = canvas.star
280
283
  chevron = canvas.chevron
281
284
 
@@ -191,6 +191,56 @@ class CanvasLineFeature(CanvasBase):
191
191
  style=style,
192
192
  )
193
193
 
194
+ @error_handler
195
+ def line_arc(
196
+ self,
197
+ xy: Tuple[float, float],
198
+ width: float,
199
+ height: float,
200
+ from_angle: float = 0,
201
+ to_angle: float = 180,
202
+ lwidth: Optional[float] = None,
203
+ arrowhead: Literal["", "->", "<-", "<->"] = "",
204
+ style: Union[LineStyle, str, None] = None,
205
+ ) -> None:
206
+ """Draw arc line on ellipse.
207
+
208
+ Args:
209
+ xy: Tuple[float, float]: The center point of the ellipse from which the arc is drawn.
210
+ width: float: The width of the ellipse.
211
+ height: float: The height of the ellipse
212
+ from_angle: float: The starting angle of the arc in degrees (default is 0).
213
+ to_angle: float: The ending angle of the arc in degrees (default is 180).
214
+ lwidth: Optional[float]: Optional width of the line.
215
+ arrowhead: Literal["", "->", "<-", "<->"]: Optional arrowhead style ("", "->", "<-", "<->").
216
+ style: Union[LineStyle, str, None]: Optional line style.
217
+
218
+ Returns:
219
+ None
220
+ """
221
+ style = LineUtil.format_style(style)
222
+ # validator.validate_line_args(locals())
223
+
224
+ if from_angle > to_angle:
225
+ from_angle = -1 * (360 - from_angle)
226
+ path_points = LineArcHelper.get_ellipse_path_points(
227
+ xy,
228
+ width,
229
+ height,
230
+ from_angle,
231
+ to_angle,
232
+ )
233
+ start_point = path_points[0]
234
+ path_points = path_points[1:]
235
+
236
+ self.lines_bezier(
237
+ start_point, # type: ignore
238
+ path_points=path_points, # type: ignore
239
+ width=lwidth,
240
+ arrowhead=arrowhead,
241
+ style=style,
242
+ )
243
+
194
244
  @error_handler
195
245
  def lines(
196
246
  self,
@@ -332,6 +382,141 @@ class CanvasLineFeature(CanvasBase):
332
382
  self._artists.append(FancyArrowPatch(path=path, **options))
333
383
 
334
384
 
385
+ class LineArcHelper:
386
+ """Internal class"""
387
+
388
+ @classmethod
389
+ def get_point_on_ellipse(
390
+ cls,
391
+ xy: Tuple[float, float],
392
+ width: float,
393
+ height: float,
394
+ angle: float,
395
+ ) -> Tuple[float, float]:
396
+ """Internal function"""
397
+ x, y = xy
398
+ # Convert angle from degrees to radians
399
+ angle = math.radians(angle)
400
+
401
+ # Calculate the coordinates of the point
402
+ point_x = x + width / 2 * math.cos(angle)
403
+ point_y = y + height / 2 * math.sin(angle)
404
+
405
+ return point_x, point_y
406
+
407
+ @classmethod
408
+ def get_ellipse_path_points(
409
+ cls,
410
+ xy: Tuple[float, float],
411
+ width: float,
412
+ height: float,
413
+ from_angle: float,
414
+ to_angle: float,
415
+ ) -> List[
416
+ Union[
417
+ Tuple[float, float],
418
+ Tuple[Tuple[float, float], Tuple[float, float], Tuple[float, float]],
419
+ ]
420
+ ]:
421
+ """Internal function"""
422
+ diff = to_angle - from_angle
423
+ if abs(diff) > 270:
424
+ # having +2 for avoiding situation next_mid_angle == to_angle
425
+ step = int(diff / 3)
426
+ elif abs(diff) > 135:
427
+ step = int(diff / 2)
428
+ else:
429
+ p1, p2, p3, p4 = cls.bezier_ellipse_arc_approximation(
430
+ xy,
431
+ width,
432
+ height,
433
+ from_angle,
434
+ to_angle,
435
+ )
436
+ return [p1, (p2, p3, p4)]
437
+ if diff >= 0:
438
+ diff += 2
439
+ else:
440
+ diff -= 2
441
+
442
+ i = 0
443
+ start = None
444
+ path_points = []
445
+ while True:
446
+ last_mid_angle = from_angle + step * i
447
+ next_mid_angle = from_angle + step * (i + 1)
448
+
449
+ if from_angle < to_angle:
450
+ # anti clock wise
451
+ is_last = next_mid_angle > to_angle
452
+ else:
453
+ # clock wise
454
+ is_last = to_angle > next_mid_angle
455
+
456
+ if is_last:
457
+ p1, p2, p3, p4 = cls.bezier_ellipse_arc_approximation(
458
+ xy,
459
+ width,
460
+ height,
461
+ last_mid_angle,
462
+ to_angle,
463
+ )
464
+ if start is None:
465
+ start = p1
466
+ path_points.append((p2, p3, p4))
467
+ break
468
+
469
+ else:
470
+ p1, p2, p3, p4 = cls.bezier_ellipse_arc_approximation(
471
+ xy,
472
+ width,
473
+ height,
474
+ last_mid_angle,
475
+ next_mid_angle,
476
+ )
477
+ if start is None:
478
+ start = p1
479
+ path_points.append((p2, p3, p4))
480
+
481
+ i += 1
482
+
483
+ path_points.insert(0, start)
484
+ return path_points
485
+
486
+ @classmethod
487
+ def bezier_ellipse_arc_approximation(
488
+ cls,
489
+ xy: Tuple[float, float],
490
+ width: float,
491
+ height: float,
492
+ start_angle: float,
493
+ end_angle: float,
494
+ ) -> Tuple[Tuple[float, float], Tuple[float, float], Tuple[float, float], Tuple[float, float]]:
495
+ """Internal function"""
496
+ x, y = xy
497
+
498
+ # Convert angles from degrees to radians
499
+ start_angle = math.radians(start_angle)
500
+ end_angle = math.radians(end_angle)
501
+
502
+ # Calculate the points on the ellipse at the start and end angles
503
+ start_point = (x + width / 2 * math.cos(start_angle), y + height / 2 * math.sin(start_angle))
504
+ end_point = (x + width / 2 * math.cos(end_angle), y + height / 2 * math.sin(end_angle))
505
+
506
+ # Calculate the control points for the Bezier curve
507
+ t = (4 / 3) * math.tan((end_angle - start_angle) / 4)
508
+ control_point1 = (
509
+ start_point[0] - t * width / 2 * math.sin(start_angle),
510
+ start_point[1] + t * height / 2 * math.cos(start_angle),
511
+ )
512
+ control_point2 = (
513
+ end_point[0] + t * width / 2 * math.sin(end_angle),
514
+ end_point[1] - t * height / 2 * math.cos(end_angle),
515
+ )
516
+
517
+ return start_point, control_point1, control_point2, end_point
518
+
519
+
335
520
  def _get_mid_points(
336
521
  a: Tuple[float, float],
337
522
  b: Tuple[float, float],