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 +1 -1
- drawlib/v0_2/apis.py +3 -0
- drawlib/v0_2/private/core/model.py +19 -1
- drawlib/v0_2/private/core/util.py +6 -0
- drawlib/v0_2/private/core_canvas/canvas.py +3 -0
- drawlib/v0_2/private/core_canvas/line.py +185 -0
- drawlib/v0_2/private/core_canvas/original_arrow.py +498 -1
- drawlib/v0_2/private/smartarts/boxlist.py +178 -0
- drawlib/v0_2/private/smartarts/bulletpoints.py +188 -0
- drawlib/v0_2/private/smartarts/dsart.py +7 -0
- drawlib/v0_2/private/smartarts/gridlayout.py +47 -26
- drawlib/v0_2/private/smartarts/pyramid.py +229 -17
- drawlib/v0_2/private/smartarts/table.py +683 -0
- drawlib/v0_2/private/smartarts/tree.py +10 -5
- drawlib/v0_2/private/validators/args.py +18 -3
- {drawlib-0.2.3.dev2.dist-info → drawlib-0.2.3.dev3.dist-info}/METADATA +1 -1
- {drawlib-0.2.3.dev2.dist-info → drawlib-0.2.3.dev3.dist-info}/RECORD +20 -17
- {drawlib-0.2.3.dev2.dist-info → drawlib-0.2.3.dev3.dist-info}/LICENSE.txt +0 -0
- {drawlib-0.2.3.dev2.dist-info → drawlib-0.2.3.dev3.dist-info}/WHEEL +0 -0
- {drawlib-0.2.3.dev2.dist-info → drawlib-0.2.3.dev3.dist-info}/entry_points.txt +0 -0
|
@@ -10,13 +10,18 @@
|
|
|
10
10
|
|
|
11
11
|
"""Canvas's original arrow feature implementation module."""
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
import math
|
|
14
|
+
from typing import List, Literal, Optional, Tuple, Union
|
|
15
|
+
|
|
16
|
+
from matplotlib.patches import PathPatch
|
|
17
|
+
from matplotlib.path import Path
|
|
14
18
|
|
|
15
19
|
import drawlib.v0_2.private.validators.args as validator
|
|
16
20
|
from drawlib.v0_2.private.core.model import ShapeStyle, ShapeTextStyle
|
|
17
21
|
from drawlib.v0_2.private.core.theme import dtheme
|
|
18
22
|
from drawlib.v0_2.private.core.util import ShapeUtil
|
|
19
23
|
from drawlib.v0_2.private.core_canvas.base import CanvasBase
|
|
24
|
+
from drawlib.v0_2.private.core_canvas.line import LineArcHelper
|
|
20
25
|
from drawlib.v0_2.private.util import (
|
|
21
26
|
error_handler,
|
|
22
27
|
get_angle,
|
|
@@ -137,3 +142,495 @@ class CanvasOriginalArrowFeature(CanvasBase):
|
|
|
137
142
|
textsize=textsize,
|
|
138
143
|
textstyle=textstyle,
|
|
139
144
|
)
|
|
145
|
+
|
|
146
|
+
@error_handler
|
|
147
|
+
def arrows(
|
|
148
|
+
self,
|
|
149
|
+
xys: List[Tuple[float, float]],
|
|
150
|
+
tail_width: float,
|
|
151
|
+
head_width: float,
|
|
152
|
+
head_length: float,
|
|
153
|
+
head: Literal[
|
|
154
|
+
"->",
|
|
155
|
+
"<-",
|
|
156
|
+
"<->",
|
|
157
|
+
] = "->",
|
|
158
|
+
r: float = 0,
|
|
159
|
+
style: Union[ShapeStyle, str, None] = None,
|
|
160
|
+
) -> None:
|
|
161
|
+
"""Draw single and double-headed arrow.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
xys: List[Tuple[float, float]]: Arrow points.
|
|
165
|
+
tail_width: float: Width of the arrow tail.
|
|
166
|
+
head_width: float: Width of the arrow head.
|
|
167
|
+
head_length: float: Length of the arrow head.
|
|
168
|
+
head: Literal["->", "<-", "<->"]: Arrow head style ("->", "<-", "<->").
|
|
169
|
+
r (float, optional): Radius for rounded connections (default is 0.0).
|
|
170
|
+
style: Union[ShapeStyle, str, None]: Optional style of the arrow.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
None
|
|
174
|
+
"""
|
|
175
|
+
# matplotlib FancyArrow, FancyArrowPatch seems not good
|
|
176
|
+
# for implement this function.
|
|
177
|
+
# Calculate arrow points pass it to shape().
|
|
178
|
+
|
|
179
|
+
style, _ = ShapeUtil.format_styles(
|
|
180
|
+
style,
|
|
181
|
+
None,
|
|
182
|
+
dtheme.arrowstyles.get,
|
|
183
|
+
dtheme.arrowtextstyles.get,
|
|
184
|
+
)
|
|
185
|
+
validator.validate_shape_args(locals())
|
|
186
|
+
|
|
187
|
+
parallel_xys1 = ArrowsHelper.get_parallel_lines_xys(xys, tail_width / 2)
|
|
188
|
+
parallel_xys2 = ArrowsHelper.get_parallel_lines_xys(xys, tail_width / 2 * -1)
|
|
189
|
+
|
|
190
|
+
arrowhead_start_tail_xy1 = ArrowsHelper.get_point_on_line(parallel_xys1[0], parallel_xys1[1], head_length)
|
|
191
|
+
arrowhead_start_tail_xy2 = ArrowsHelper.get_point_on_line(parallel_xys2[0], parallel_xys2[1], head_length)
|
|
192
|
+
arrowhead_end_tail_xy1 = ArrowsHelper.get_point_on_line(
|
|
193
|
+
parallel_xys1[len(parallel_xys1) - 1],
|
|
194
|
+
parallel_xys1[len(parallel_xys1) - 2],
|
|
195
|
+
head_length,
|
|
196
|
+
)
|
|
197
|
+
arrowhead_end_tail_xy2 = ArrowsHelper.get_point_on_line(
|
|
198
|
+
parallel_xys2[len(parallel_xys1) - 1],
|
|
199
|
+
parallel_xys2[len(parallel_xys1) - 2],
|
|
200
|
+
head_length,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
xy1, xy2 = ArrowsHelper.get_parallel_line_xys(xys[0], xys[1], head_width / 2)
|
|
204
|
+
arrowhead_start_head_xy1 = ArrowsHelper.get_point_on_line(xy1, xy2, head_length)
|
|
205
|
+
xy1, xy2 = ArrowsHelper.get_parallel_line_xys(xys[0], xys[1], head_width / 2 * -1)
|
|
206
|
+
arrowhead_start_head_xy2 = ArrowsHelper.get_point_on_line(xy1, xy2, head_length)
|
|
207
|
+
xy1, xy2 = ArrowsHelper.get_parallel_line_xys(
|
|
208
|
+
xys[len(xys) - 1], xys[len(xys) - 2], head_width / 2 * -1
|
|
209
|
+
) # since poinrts are reversed, having * -1 here
|
|
210
|
+
arrowhead_end_head_xy1 = ArrowsHelper.get_point_on_line(xy1, xy2, head_length)
|
|
211
|
+
xy1, xy2 = ArrowsHelper.get_parallel_line_xys(xys[len(xys) - 1], xys[len(xys) - 2], head_width / 2)
|
|
212
|
+
arrowhead_end_head_xy2 = ArrowsHelper.get_point_on_line(xy1, xy2, head_length)
|
|
213
|
+
|
|
214
|
+
points1: List[Tuple[float, float]] = []
|
|
215
|
+
if head == "->":
|
|
216
|
+
points1.append(parallel_xys1[0])
|
|
217
|
+
else:
|
|
218
|
+
points1.append(arrowhead_start_tail_xy1)
|
|
219
|
+
points1.extend(parallel_xys1[1:-1])
|
|
220
|
+
if head == "<-":
|
|
221
|
+
points1.append(parallel_xys1[len(parallel_xys1) - 1])
|
|
222
|
+
else:
|
|
223
|
+
points1.append(arrowhead_end_tail_xy1)
|
|
224
|
+
|
|
225
|
+
points2: List[Tuple[float, float]] = []
|
|
226
|
+
if head == "<-":
|
|
227
|
+
points2.append(parallel_xys2[len(parallel_xys2) - 1])
|
|
228
|
+
else:
|
|
229
|
+
points2.append(arrowhead_end_tail_xy2)
|
|
230
|
+
temp = parallel_xys2[1:-1]
|
|
231
|
+
temp.reverse()
|
|
232
|
+
points2.extend(temp)
|
|
233
|
+
if head == "->":
|
|
234
|
+
points2.append(parallel_xys2[0])
|
|
235
|
+
else:
|
|
236
|
+
points2.append(arrowhead_start_tail_xy2)
|
|
237
|
+
|
|
238
|
+
# apply r here
|
|
239
|
+
if r != 0:
|
|
240
|
+
path_points1: List[
|
|
241
|
+
Union[
|
|
242
|
+
Tuple[float, float],
|
|
243
|
+
Tuple[Tuple[float, float], Tuple[float, float]],
|
|
244
|
+
Tuple[Tuple[float, float], Tuple[float, float], Tuple[float, float]],
|
|
245
|
+
]
|
|
246
|
+
] = ArrowsHelper.get_path_points(points1, r)
|
|
247
|
+
path_points2: List[
|
|
248
|
+
Union[
|
|
249
|
+
Tuple[float, float],
|
|
250
|
+
Tuple[Tuple[float, float], Tuple[float, float]],
|
|
251
|
+
Tuple[Tuple[float, float], Tuple[float, float], Tuple[float, float]],
|
|
252
|
+
]
|
|
253
|
+
] = ArrowsHelper.get_path_points(points2, r)
|
|
254
|
+
else:
|
|
255
|
+
path_points1 = points1 # type: ignore
|
|
256
|
+
path_points2 = points2 # type: ignore
|
|
257
|
+
|
|
258
|
+
if head != "->":
|
|
259
|
+
path_points1.insert(0, xys[0])
|
|
260
|
+
path_points1.insert(1, arrowhead_start_head_xy1)
|
|
261
|
+
if head != "<-":
|
|
262
|
+
path_points1.append(arrowhead_end_head_xy1)
|
|
263
|
+
path_points1.append(xys[len(xys) - 1])
|
|
264
|
+
|
|
265
|
+
if head != "<-":
|
|
266
|
+
path_points2.insert(0, arrowhead_end_head_xy2)
|
|
267
|
+
if head != "->":
|
|
268
|
+
path_points2.append(arrowhead_start_head_xy2)
|
|
269
|
+
|
|
270
|
+
path_points1.extend(path_points2)
|
|
271
|
+
|
|
272
|
+
# create Path
|
|
273
|
+
vertices = [path_points1[0]]
|
|
274
|
+
codes = [Path.MOVETO]
|
|
275
|
+
for p in path_points1[1:]:
|
|
276
|
+
length = len(p)
|
|
277
|
+
if length not in {2, 3}:
|
|
278
|
+
raise ValueError()
|
|
279
|
+
|
|
280
|
+
if not isinstance(p[0], tuple):
|
|
281
|
+
vertices.append(p)
|
|
282
|
+
codes.append(Path.LINETO)
|
|
283
|
+
|
|
284
|
+
elif length == 2:
|
|
285
|
+
vertices.extend([p[0], p[1]]) # type: ignore
|
|
286
|
+
codes.extend([Path.CURVE3] * 2)
|
|
287
|
+
|
|
288
|
+
else:
|
|
289
|
+
vertices.extend([p[0], p[1], p[2]]) # type: ignore
|
|
290
|
+
codes.extend([Path.CURVE4] * 3)
|
|
291
|
+
|
|
292
|
+
vertices.append(path_points1[0])
|
|
293
|
+
codes.append(Path.CLOSEPOLY)
|
|
294
|
+
path = Path(vertices=vertices, codes=codes)
|
|
295
|
+
|
|
296
|
+
# create PathPatch
|
|
297
|
+
|
|
298
|
+
options = ShapeUtil.get_shape_options(style)
|
|
299
|
+
self._artists.append(PathPatch(path=path, **options))
|
|
300
|
+
|
|
301
|
+
@error_handler
|
|
302
|
+
def arrow_arc(
|
|
303
|
+
self,
|
|
304
|
+
xy: Tuple[float, float],
|
|
305
|
+
width: float,
|
|
306
|
+
height: float,
|
|
307
|
+
tail_width: float,
|
|
308
|
+
head_width: float,
|
|
309
|
+
head_angle: float = 10,
|
|
310
|
+
head: Literal["->", "<-", "<->"] = "->",
|
|
311
|
+
from_angle: float = 0,
|
|
312
|
+
to_angle: float = 180,
|
|
313
|
+
style: Union[ShapeStyle, str, None] = None,
|
|
314
|
+
) -> None:
|
|
315
|
+
"""Draw arc arrow.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
xy: Tuple[float, float]: The center point of the circle from which the arc arrow is drawn.
|
|
319
|
+
width: float: The width of the ellipse.
|
|
320
|
+
height: float: The height of the ellipse
|
|
321
|
+
tail_width: float: The width of the tail of the arrow.
|
|
322
|
+
head_width: float: The width of the head of the arrow.
|
|
323
|
+
head_angle: float: The angle of the arrowhead in degrees (default is 10).
|
|
324
|
+
head: Literal["->", "<-", "<->"]: The arrowhead style ("->", "<-", "<->").
|
|
325
|
+
from_angle: float: The starting angle of the arc in degrees (default is 0).
|
|
326
|
+
to_angle: float: The ending angle of the arc in degrees (default is 180).
|
|
327
|
+
style: Union[ShapeStyle, str, None]: Optional shape style.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
None
|
|
331
|
+
"""
|
|
332
|
+
style, _ = ShapeUtil.format_styles(
|
|
333
|
+
style,
|
|
334
|
+
None,
|
|
335
|
+
dtheme.arrowstyles.get,
|
|
336
|
+
dtheme.arrowtextstyles.get,
|
|
337
|
+
)
|
|
338
|
+
validator.validate_shape_args(locals())
|
|
339
|
+
|
|
340
|
+
width_int = width - tail_width
|
|
341
|
+
width_ext = width + tail_width
|
|
342
|
+
height_int = height - tail_width
|
|
343
|
+
height_ext = height + tail_width
|
|
344
|
+
|
|
345
|
+
if head in {"<-", "<->"}:
|
|
346
|
+
from_angle2 = from_angle + head_angle
|
|
347
|
+
from_angle2 %= 360
|
|
348
|
+
else:
|
|
349
|
+
from_angle2 = from_angle
|
|
350
|
+
|
|
351
|
+
if head in {"->", "<->"}:
|
|
352
|
+
to_angle2 = to_angle - head_angle
|
|
353
|
+
to_angle2 %= 360
|
|
354
|
+
else:
|
|
355
|
+
to_angle2 = to_angle
|
|
356
|
+
|
|
357
|
+
if from_angle2 > to_angle2:
|
|
358
|
+
pp1_fa = -1 * (360 - from_angle2)
|
|
359
|
+
pp1_ta = to_angle2
|
|
360
|
+
pp2_fa = to_angle2
|
|
361
|
+
pp2_ta = pp1_fa
|
|
362
|
+
else:
|
|
363
|
+
pp1_fa = from_angle2
|
|
364
|
+
pp1_ta = to_angle2
|
|
365
|
+
pp2_fa = to_angle2
|
|
366
|
+
pp2_ta = from_angle2
|
|
367
|
+
|
|
368
|
+
path_points1 = LineArcHelper.get_ellipse_path_points(
|
|
369
|
+
xy,
|
|
370
|
+
width_int,
|
|
371
|
+
height_int,
|
|
372
|
+
pp1_fa,
|
|
373
|
+
pp1_ta,
|
|
374
|
+
)
|
|
375
|
+
path_points2 = LineArcHelper.get_ellipse_path_points(
|
|
376
|
+
xy,
|
|
377
|
+
width_ext,
|
|
378
|
+
height_ext,
|
|
379
|
+
pp2_fa,
|
|
380
|
+
pp2_ta,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
if from_angle > to_angle:
|
|
384
|
+
from_angle = -1 * (360 - from_angle)
|
|
385
|
+
|
|
386
|
+
if head in {"<-", "<->"}:
|
|
387
|
+
path_points1.insert(
|
|
388
|
+
0,
|
|
389
|
+
LineArcHelper.get_point_on_ellipse(
|
|
390
|
+
xy,
|
|
391
|
+
width,
|
|
392
|
+
height,
|
|
393
|
+
from_angle,
|
|
394
|
+
),
|
|
395
|
+
)
|
|
396
|
+
path_points1.insert(
|
|
397
|
+
1,
|
|
398
|
+
LineArcHelper.get_point_on_ellipse(
|
|
399
|
+
xy,
|
|
400
|
+
width - head_width,
|
|
401
|
+
height - head_width,
|
|
402
|
+
from_angle2,
|
|
403
|
+
),
|
|
404
|
+
)
|
|
405
|
+
path_points2.append(
|
|
406
|
+
LineArcHelper.get_point_on_ellipse(
|
|
407
|
+
xy,
|
|
408
|
+
width + head_width,
|
|
409
|
+
height + head_width,
|
|
410
|
+
from_angle2,
|
|
411
|
+
)
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
if head in {"->", "<->"}:
|
|
415
|
+
path_points1.append(
|
|
416
|
+
LineArcHelper.get_point_on_ellipse(
|
|
417
|
+
xy,
|
|
418
|
+
width - head_width,
|
|
419
|
+
height - head_width,
|
|
420
|
+
to_angle2,
|
|
421
|
+
)
|
|
422
|
+
)
|
|
423
|
+
path_points1.append(
|
|
424
|
+
LineArcHelper.get_point_on_ellipse(
|
|
425
|
+
xy,
|
|
426
|
+
width,
|
|
427
|
+
height,
|
|
428
|
+
to_angle,
|
|
429
|
+
)
|
|
430
|
+
)
|
|
431
|
+
path_points2.insert(
|
|
432
|
+
0,
|
|
433
|
+
LineArcHelper.get_point_on_ellipse(
|
|
434
|
+
xy,
|
|
435
|
+
width + head_width,
|
|
436
|
+
height + head_width,
|
|
437
|
+
to_angle2,
|
|
438
|
+
),
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
path_points1.extend(path_points2)
|
|
442
|
+
|
|
443
|
+
# create Path
|
|
444
|
+
vertices = [path_points1[0]]
|
|
445
|
+
codes = [Path.MOVETO]
|
|
446
|
+
for p in path_points1[1:]:
|
|
447
|
+
length = len(p)
|
|
448
|
+
if length not in {2, 3}:
|
|
449
|
+
raise ValueError()
|
|
450
|
+
|
|
451
|
+
if not isinstance(p[0], tuple):
|
|
452
|
+
vertices.append(p)
|
|
453
|
+
codes.append(Path.LINETO)
|
|
454
|
+
|
|
455
|
+
elif length == 2:
|
|
456
|
+
vertices.extend([p[0], p[1]]) # type: ignore
|
|
457
|
+
codes.extend([Path.CURVE3] * 2)
|
|
458
|
+
|
|
459
|
+
else:
|
|
460
|
+
vertices.extend([p[0], p[1], p[2]]) # type: ignore
|
|
461
|
+
codes.extend([Path.CURVE4] * 3)
|
|
462
|
+
|
|
463
|
+
vertices.append(path_points1[0])
|
|
464
|
+
codes.append(Path.CLOSEPOLY)
|
|
465
|
+
path = Path(vertices=vertices, codes=codes)
|
|
466
|
+
|
|
467
|
+
# create PathPatch
|
|
468
|
+
|
|
469
|
+
options = ShapeUtil.get_shape_options(style)
|
|
470
|
+
self._artists.append(PathPatch(path=path, **options))
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
class ArrowsHelper:
|
|
474
|
+
"""Internal class"""
|
|
475
|
+
|
|
476
|
+
@classmethod
|
|
477
|
+
def get_path_points(
|
|
478
|
+
cls,
|
|
479
|
+
xys: List[Tuple[float, float]],
|
|
480
|
+
r: float,
|
|
481
|
+
) -> List[
|
|
482
|
+
Union[
|
|
483
|
+
Tuple[float, float],
|
|
484
|
+
Tuple[Tuple[float, float], Tuple[float, float]],
|
|
485
|
+
Tuple[Tuple[float, float], Tuple[float, float], Tuple[float, float]],
|
|
486
|
+
]
|
|
487
|
+
]:
|
|
488
|
+
"""Internal function"""
|
|
489
|
+
|
|
490
|
+
def _get_mid_points(
|
|
491
|
+
a: Tuple[float, float],
|
|
492
|
+
b: Tuple[float, float],
|
|
493
|
+
r: float,
|
|
494
|
+
) -> Tuple[Tuple[float, float], Tuple[float, float]]:
|
|
495
|
+
# Calculate the vector from A to B
|
|
496
|
+
ab = [b[0] - a[0], b[1] - a[1]]
|
|
497
|
+
|
|
498
|
+
# Calculate the distance from A to B
|
|
499
|
+
ab_distance = math.sqrt(ab[0] ** 2 + ab[1] ** 2)
|
|
500
|
+
|
|
501
|
+
# Normalize the vector AB to get the unit vector
|
|
502
|
+
ab_unit = [ab[0] / ab_distance, ab[1] / ab_distance]
|
|
503
|
+
|
|
504
|
+
# Calculate point C: A + X * unit vector AB
|
|
505
|
+
c = (a[0] + r * ab_unit[0], a[1] + r * ab_unit[1])
|
|
506
|
+
|
|
507
|
+
# Calculate point D: B - X * unit vector AB
|
|
508
|
+
d = (b[0] - r * ab_unit[0], b[1] - r * ab_unit[1])
|
|
509
|
+
|
|
510
|
+
return c, d
|
|
511
|
+
|
|
512
|
+
path_points: List[
|
|
513
|
+
Union[
|
|
514
|
+
Tuple[float, float],
|
|
515
|
+
Tuple[Tuple[float, float], Tuple[float, float]],
|
|
516
|
+
Tuple[Tuple[float, float], Tuple[float, float], Tuple[float, float]],
|
|
517
|
+
]
|
|
518
|
+
] = []
|
|
519
|
+
|
|
520
|
+
last_i = len(xys) - 2
|
|
521
|
+
# last_xy = (0, 0)
|
|
522
|
+
for i, xy in enumerate(xys):
|
|
523
|
+
if i == 0:
|
|
524
|
+
_, p = _get_mid_points(xy, xys[i + 1], r)
|
|
525
|
+
path_points.append(p)
|
|
526
|
+
continue
|
|
527
|
+
|
|
528
|
+
if i == last_i:
|
|
529
|
+
p, _ = _get_mid_points(xy, xys[i + 1], r)
|
|
530
|
+
path_points.append((xy, p))
|
|
531
|
+
path_points.append(xys[i + 1])
|
|
532
|
+
break
|
|
533
|
+
|
|
534
|
+
p1, p2 = _get_mid_points(xy, xys[i + 1], r)
|
|
535
|
+
path_points.append((xy, p1))
|
|
536
|
+
path_points.append(p2)
|
|
537
|
+
|
|
538
|
+
path_points.insert(0, xys[0])
|
|
539
|
+
return path_points
|
|
540
|
+
|
|
541
|
+
@classmethod
|
|
542
|
+
def get_point_on_line(
|
|
543
|
+
cls,
|
|
544
|
+
xy1: Tuple[float, float],
|
|
545
|
+
xy2: Tuple[float, float],
|
|
546
|
+
distance: float,
|
|
547
|
+
) -> Tuple[float, float]:
|
|
548
|
+
"""Internal function"""
|
|
549
|
+
x1, y1 = xy1
|
|
550
|
+
x2, y2 = xy2
|
|
551
|
+
|
|
552
|
+
v_length = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
|
|
553
|
+
u_x = (x2 - x1) / v_length
|
|
554
|
+
u_y = (y2 - y1) / v_length
|
|
555
|
+
|
|
556
|
+
x = x1 + distance * u_x
|
|
557
|
+
y = y1 + distance * u_y
|
|
558
|
+
|
|
559
|
+
return (x, y)
|
|
560
|
+
|
|
561
|
+
@classmethod
|
|
562
|
+
def get_parallel_lines_xys(
|
|
563
|
+
cls,
|
|
564
|
+
xys: List[Tuple[float, float]],
|
|
565
|
+
distance: float,
|
|
566
|
+
) -> List[Tuple[float, float]]:
|
|
567
|
+
"""Internal function"""
|
|
568
|
+
parallel_lines: List[Tuple[Tuple[float, float], Tuple[float, float]]] = []
|
|
569
|
+
for i in range(len(xys) - 1):
|
|
570
|
+
xy1, xy2 = cls.get_parallel_line_xys(xys[i], xys[i + 1], distance)
|
|
571
|
+
parallel_lines.append((xy1, xy2))
|
|
572
|
+
|
|
573
|
+
points: List[Tuple[float, float]] = []
|
|
574
|
+
for i in range(len(parallel_lines) - 1):
|
|
575
|
+
xy1, xy2 = parallel_lines[i]
|
|
576
|
+
xy3, xy4 = parallel_lines[i + 1]
|
|
577
|
+
xy = cls.find_lines_intersection(xy1, xy2, xy3, xy4)
|
|
578
|
+
points.append(xy)
|
|
579
|
+
|
|
580
|
+
first_point = parallel_lines[0][0]
|
|
581
|
+
last_point = parallel_lines[len(parallel_lines) - 1][1]
|
|
582
|
+
|
|
583
|
+
points.insert(0, first_point)
|
|
584
|
+
points.append(last_point)
|
|
585
|
+
|
|
586
|
+
return points
|
|
587
|
+
|
|
588
|
+
@classmethod
|
|
589
|
+
def get_parallel_line_xys(
|
|
590
|
+
cls,
|
|
591
|
+
xy1: Tuple[float, float],
|
|
592
|
+
xy2: Tuple[float, float],
|
|
593
|
+
distance: float,
|
|
594
|
+
) -> Tuple[Tuple[float, float], Tuple[float, float]]:
|
|
595
|
+
"""Internal function"""
|
|
596
|
+
x1, y1 = xy1
|
|
597
|
+
x2, y2 = xy2
|
|
598
|
+
|
|
599
|
+
# calc vector
|
|
600
|
+
vx = x2 - x1
|
|
601
|
+
vy = y2 - y1
|
|
602
|
+
length = math.sqrt(vx**2 + vy**2)
|
|
603
|
+
dx = -vy / length * distance
|
|
604
|
+
dy = vx / length * distance
|
|
605
|
+
|
|
606
|
+
# add vector
|
|
607
|
+
x1_prime = x1 + dx
|
|
608
|
+
y1_prime = y1 + dy
|
|
609
|
+
x2_prime = x2 + dx
|
|
610
|
+
y2_prime = y2 + dy
|
|
611
|
+
|
|
612
|
+
return (x1_prime, y1_prime), (x2_prime, y2_prime)
|
|
613
|
+
|
|
614
|
+
@classmethod
|
|
615
|
+
def find_lines_intersection(
|
|
616
|
+
cls,
|
|
617
|
+
xy1: Tuple[float, float],
|
|
618
|
+
xy2: Tuple[float, float],
|
|
619
|
+
xy3: Tuple[float, float],
|
|
620
|
+
xy4: Tuple[float, float],
|
|
621
|
+
) -> Tuple[float, float]:
|
|
622
|
+
"""Internal function"""
|
|
623
|
+
x1, y1 = xy1
|
|
624
|
+
x2, y2 = xy2
|
|
625
|
+
x3, y3 = xy3
|
|
626
|
+
x4, y4 = xy4
|
|
627
|
+
|
|
628
|
+
m1 = (y2 - y1) / (x2 - x1)
|
|
629
|
+
b1 = y1 - m1 * x1
|
|
630
|
+
m2 = (y4 - y3) / (x4 - x3)
|
|
631
|
+
b2 = y3 - m2 * x3
|
|
632
|
+
|
|
633
|
+
x = (b2 - b1) / (m1 - m2)
|
|
634
|
+
y = m1 * x + b1
|
|
635
|
+
|
|
636
|
+
return (x, y)
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Copyright (c) 2024 Yuichi Ito (yuichi@yuichi.com)
|
|
2
|
+
#
|
|
3
|
+
# This software is licensed under the Apache License, Version 2.0.
|
|
4
|
+
# For more information, please visit: https://github.com/yuichi110/drawlib
|
|
5
|
+
#
|
|
6
|
+
# This software is provided "as is", without warranty of any kind,
|
|
7
|
+
# express or implied, including but not limited to the warranties of
|
|
8
|
+
# merchantability, fitness for a particular purpose and noninfringement.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
"""BoxList implementation module."""
|
|
12
|
+
|
|
13
|
+
from typing import Any, List, Literal, Optional, Tuple, Union
|
|
14
|
+
|
|
15
|
+
from drawlib.v0_2.private.core.colors import Colors
|
|
16
|
+
from drawlib.v0_2.private.core.model import ShapeStyle, ShapeTextStyle
|
|
17
|
+
from drawlib.v0_2.private.core.theme import dtheme
|
|
18
|
+
from drawlib.v0_2.private.core_canvas.canvas import rectangle
|
|
19
|
+
from drawlib.v0_2.private.util import error_handler
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BoxList:
|
|
23
|
+
"""A class to draw a list of boxes with text, supporting highlighting of certain boxes.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
box_style (Union[str, ShapeStyle, None]): The style for the boxes.
|
|
27
|
+
text_style (Union[str, ShapeTextStyle, None]): The style for the text inside the boxes.
|
|
28
|
+
box_highlight_style (Union[str, ShapeStyle, None]): The style for the highlighted boxes.
|
|
29
|
+
text_highlight_style (Union[str, ShapeTextStyle, None]): The style for the text inside the highlighted boxes.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
@error_handler
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
box_style: Union[str, ShapeStyle, None] = None,
|
|
36
|
+
text_style: Union[str, ShapeTextStyle, None] = None,
|
|
37
|
+
box_highlight_style: Union[str, ShapeStyle, None] = None,
|
|
38
|
+
text_highlight_style: Union[str, ShapeTextStyle, None] = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Initialize BoxList.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
box_style (Union[str, ShapeStyle, None]): The style for the boxes.
|
|
44
|
+
text_style (Union[str, ShapeTextStyle, None]): The style for the text inside the boxes.
|
|
45
|
+
box_highlight_style (Union[str, ShapeStyle, None]): The style for the highlighted boxes.
|
|
46
|
+
text_highlight_style (Union[str, ShapeTextStyle, None]):
|
|
47
|
+
The style for the text inside the highlighted boxes.
|
|
48
|
+
"""
|
|
49
|
+
if isinstance(box_style, str):
|
|
50
|
+
box_style = dtheme.rectanglestyles.get(box_style)
|
|
51
|
+
if box_style is None:
|
|
52
|
+
box_style = dtheme.rectanglestyles.get()
|
|
53
|
+
box_style.halign = "center"
|
|
54
|
+
box_style.valign = "center"
|
|
55
|
+
self._box_style = box_style
|
|
56
|
+
|
|
57
|
+
if isinstance(text_style, str):
|
|
58
|
+
text_style = dtheme.rectangletextstyles.get(text_style)
|
|
59
|
+
if text_style is None:
|
|
60
|
+
text_style = dtheme.rectangletextstyles.get()
|
|
61
|
+
self._text_style = text_style
|
|
62
|
+
|
|
63
|
+
if isinstance(box_highlight_style, str):
|
|
64
|
+
box_highlight_style = dtheme.rectanglestyles.get(box_highlight_style)
|
|
65
|
+
if box_highlight_style is None:
|
|
66
|
+
box_highlight_style = box_style
|
|
67
|
+
box_highlight_style.halign = "center"
|
|
68
|
+
box_highlight_style.valign = "center"
|
|
69
|
+
self._box_highlight_style = box_highlight_style
|
|
70
|
+
|
|
71
|
+
if isinstance(text_highlight_style, str):
|
|
72
|
+
text_highlight_style = dtheme.rectangletextstyles.get(text_highlight_style)
|
|
73
|
+
if text_highlight_style is None:
|
|
74
|
+
text_highlight_style = text_style
|
|
75
|
+
self._text_highlight_style = text_highlight_style
|
|
76
|
+
|
|
77
|
+
@error_handler
|
|
78
|
+
def draw(
|
|
79
|
+
self,
|
|
80
|
+
xy: Tuple[float, float],
|
|
81
|
+
box_width: float,
|
|
82
|
+
box_height: float,
|
|
83
|
+
items: List[Any],
|
|
84
|
+
highlight_indexs: Optional[List[int]] = None,
|
|
85
|
+
length: Optional[int] = None,
|
|
86
|
+
align: Literal["left", "right", "bottom", "top"] = "left",
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Draws a list of boxes at the specified location.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
xy (Tuple[float, float]): The starting point (x, y) to draw the list of boxes.
|
|
92
|
+
box_width (float): The width of each box.
|
|
93
|
+
box_height (float): The height of each box.
|
|
94
|
+
items (List[Any]): The list of items to be displayed inside the boxes.
|
|
95
|
+
highlight_indexs (Optional[List[int]]):
|
|
96
|
+
The list of indices of the boxes to be highlighted.
|
|
97
|
+
length (Optional[int]): The total number of boxes to be drawn.
|
|
98
|
+
align (Literal["left", "right", "bottom", "top"]):
|
|
99
|
+
The alignment of the boxes relative to the starting point.
|
|
100
|
+
|
|
101
|
+
"""
|
|
102
|
+
if highlight_indexs is None:
|
|
103
|
+
highlight_indexs = []
|
|
104
|
+
|
|
105
|
+
if length is not None:
|
|
106
|
+
indexs = range(length)
|
|
107
|
+
else:
|
|
108
|
+
indexs = range(len(items))
|
|
109
|
+
|
|
110
|
+
non_highlight_indexs = [item for item in indexs if item not in highlight_indexs]
|
|
111
|
+
highlight_indexs = [item for item in indexs if item in highlight_indexs]
|
|
112
|
+
|
|
113
|
+
for index in non_highlight_indexs:
|
|
114
|
+
if index <= len(items) - 1:
|
|
115
|
+
text = str(items[index])
|
|
116
|
+
else:
|
|
117
|
+
text = ""
|
|
118
|
+
|
|
119
|
+
self._draw_cell(
|
|
120
|
+
start_xy=xy,
|
|
121
|
+
index=index,
|
|
122
|
+
text=text,
|
|
123
|
+
box_width=box_width,
|
|
124
|
+
box_height=box_height,
|
|
125
|
+
box_style=self._box_style,
|
|
126
|
+
text_style=self._text_style,
|
|
127
|
+
align=align,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
for index in highlight_indexs:
|
|
131
|
+
if index <= len(items) - 1:
|
|
132
|
+
text = str(items[index])
|
|
133
|
+
else:
|
|
134
|
+
text = ""
|
|
135
|
+
|
|
136
|
+
self._draw_cell(
|
|
137
|
+
start_xy=xy,
|
|
138
|
+
index=index,
|
|
139
|
+
text=text,
|
|
140
|
+
box_width=box_width,
|
|
141
|
+
box_height=box_height,
|
|
142
|
+
box_style=self._box_highlight_style,
|
|
143
|
+
text_style=self._text_highlight_style,
|
|
144
|
+
align=align,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def _draw_cell(
|
|
149
|
+
start_xy: Tuple[float, float],
|
|
150
|
+
index: int,
|
|
151
|
+
text: str,
|
|
152
|
+
box_width: float,
|
|
153
|
+
box_height: float,
|
|
154
|
+
box_style: ShapeStyle,
|
|
155
|
+
text_style: ShapeTextStyle,
|
|
156
|
+
align: Literal["left", "right", "bottom", "top"],
|
|
157
|
+
) -> None:
|
|
158
|
+
if align == "left":
|
|
159
|
+
x = start_xy[0] + box_width * (index + 0.5)
|
|
160
|
+
y = start_xy[1] + box_height / 2
|
|
161
|
+
elif align == "right":
|
|
162
|
+
x = start_xy[0] - box_width * (index + 0.5)
|
|
163
|
+
y = start_xy[1] + box_height / 2
|
|
164
|
+
elif align == "bottom":
|
|
165
|
+
x = start_xy[0] + box_width / 2
|
|
166
|
+
y = start_xy[1] + box_height * (index + 0.5)
|
|
167
|
+
else:
|
|
168
|
+
x = start_xy[0] + box_width / 2
|
|
169
|
+
y = start_xy[1] - box_height * (index + 0.5)
|
|
170
|
+
|
|
171
|
+
rectangle(
|
|
172
|
+
xy=(x, y),
|
|
173
|
+
width=box_width,
|
|
174
|
+
height=box_height,
|
|
175
|
+
style=box_style,
|
|
176
|
+
text=text,
|
|
177
|
+
textstyle=text_style,
|
|
178
|
+
)
|