tesserax 0.5.1__py3-none-any.whl → 0.5.2__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.
- tesserax/__init__.py +1 -1
- tesserax/base.py +114 -0
- tesserax/layout.py +37 -58
- {tesserax-0.5.1.dist-info → tesserax-0.5.2.dist-info}/METADATA +1 -1
- tesserax-0.5.2.dist-info/RECORD +11 -0
- tesserax-0.5.1.dist-info/RECORD +0 -11
- {tesserax-0.5.1.dist-info → tesserax-0.5.2.dist-info}/WHEEL +0 -0
- {tesserax-0.5.1.dist-info → tesserax-0.5.2.dist-info}/licenses/LICENSE +0 -0
tesserax/__init__.py
CHANGED
tesserax/base.py
CHANGED
|
@@ -194,6 +194,69 @@ class Group(Shape):
|
|
|
194
194
|
|
|
195
195
|
return self
|
|
196
196
|
|
|
197
|
+
def distribute(
|
|
198
|
+
self,
|
|
199
|
+
axis: Literal["horizontal", "vertical"],
|
|
200
|
+
size: float | None = None,
|
|
201
|
+
mode: Literal["tight", "space-between", "space-around"] = "tight",
|
|
202
|
+
gap: float = 0.0,
|
|
203
|
+
) -> Self:
|
|
204
|
+
"""
|
|
205
|
+
Distributes children along an axis using rigid or flexible spacing.
|
|
206
|
+
"""
|
|
207
|
+
if not self.shapes:
|
|
208
|
+
return self
|
|
209
|
+
|
|
210
|
+
# 1. Reset and Measure
|
|
211
|
+
for s in self.shapes:
|
|
212
|
+
if isinstance(s, Spring):
|
|
213
|
+
s._size = 0.0
|
|
214
|
+
|
|
215
|
+
springs = [s for s in self.shapes if isinstance(s, Spring)]
|
|
216
|
+
total_flex = sum(s.flex for s in springs)
|
|
217
|
+
n = len(self.shapes)
|
|
218
|
+
|
|
219
|
+
# 2. Calculate Spacing Logic
|
|
220
|
+
is_horiz = axis == "horizontal"
|
|
221
|
+
rigid_total = sum(
|
|
222
|
+
s.local().width if is_horiz else s.local().height
|
|
223
|
+
for s in self.shapes
|
|
224
|
+
if not isinstance(s, Spring)
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
effective_gap = gap
|
|
228
|
+
start_offset = 0.0
|
|
229
|
+
spring_unit = 0.0
|
|
230
|
+
|
|
231
|
+
if size is not None:
|
|
232
|
+
if springs:
|
|
233
|
+
# Springs take all space not occupied by rigid shapes and fixed gaps
|
|
234
|
+
remaining = size - rigid_total - (gap * (n - 1))
|
|
235
|
+
spring_unit = max(0, remaining / total_flex) if total_flex > 0 else 0
|
|
236
|
+
elif mode == "space-between" and n > 1:
|
|
237
|
+
effective_gap = (size - rigid_total) / (n - 1)
|
|
238
|
+
elif mode == "space-around":
|
|
239
|
+
effective_gap = (size - rigid_total) / n
|
|
240
|
+
start_offset = effective_gap / 2
|
|
241
|
+
|
|
242
|
+
# 3. Apply Translations
|
|
243
|
+
cursor = start_offset
|
|
244
|
+
for s in self.shapes:
|
|
245
|
+
b = s.local()
|
|
246
|
+
|
|
247
|
+
if isinstance(s, Spring):
|
|
248
|
+
s._size = s.flex * spring_unit
|
|
249
|
+
cursor += s._size
|
|
250
|
+
else:
|
|
251
|
+
if is_horiz:
|
|
252
|
+
s.transform.tx = cursor - b.x
|
|
253
|
+
cursor += b.width + effective_gap
|
|
254
|
+
else:
|
|
255
|
+
s.transform.ty = cursor - b.y
|
|
256
|
+
cursor += b.height + effective_gap
|
|
257
|
+
|
|
258
|
+
return self
|
|
259
|
+
|
|
197
260
|
|
|
198
261
|
class Path(Shape):
|
|
199
262
|
"""
|
|
@@ -457,3 +520,54 @@ class Text(Shape):
|
|
|
457
520
|
f'fill="{self.fill}" text-anchor="{self._anchor}" dominant-baseline="middle">'
|
|
458
521
|
f"{self.content}</text>"
|
|
459
522
|
)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
class Spacer(Shape):
|
|
526
|
+
"""
|
|
527
|
+
An invisible rectangular shape used to reserve fixed space in layouts.
|
|
528
|
+
"""
|
|
529
|
+
|
|
530
|
+
def __init__(self, w: float, h: float) -> None:
|
|
531
|
+
super().__init__()
|
|
532
|
+
self.w, self.h = w, h
|
|
533
|
+
|
|
534
|
+
def local(self) -> Bounds:
|
|
535
|
+
return Bounds(0, 0, self.w, self.h)
|
|
536
|
+
|
|
537
|
+
def _render(self) -> str:
|
|
538
|
+
return ""
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
class Ghost(Shape):
|
|
542
|
+
"""
|
|
543
|
+
A shape that proxies the bounds of a target shape without rendering.
|
|
544
|
+
"""
|
|
545
|
+
|
|
546
|
+
def __init__(self, target: Shape) -> None:
|
|
547
|
+
super().__init__()
|
|
548
|
+
self.target = target
|
|
549
|
+
|
|
550
|
+
def local(self) -> Bounds:
|
|
551
|
+
"""Returns the current local bounds of the target shape."""
|
|
552
|
+
return self.target.local()
|
|
553
|
+
|
|
554
|
+
def _render(self) -> str:
|
|
555
|
+
return ""
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
class Spring(Shape):
|
|
559
|
+
"""
|
|
560
|
+
A flexible spacer that expands to fill available space in layouts.
|
|
561
|
+
"""
|
|
562
|
+
|
|
563
|
+
def __init__(self, flex: float = 1.0) -> None:
|
|
564
|
+
super().__init__()
|
|
565
|
+
self.flex = flex
|
|
566
|
+
self._size: float = 0.0
|
|
567
|
+
|
|
568
|
+
def local(self) -> Bounds:
|
|
569
|
+
# Returns a 0-width/height bound unless size is set by distribute()
|
|
570
|
+
return Bounds(0, 0, self._size, self._size)
|
|
571
|
+
|
|
572
|
+
def _render(self) -> str:
|
|
573
|
+
return ""
|
tesserax/layout.py
CHANGED
|
@@ -2,7 +2,7 @@ from abc import abstractmethod
|
|
|
2
2
|
from collections import defaultdict
|
|
3
3
|
import math
|
|
4
4
|
from typing import Literal, Self
|
|
5
|
-
from .core import Shape, Bounds
|
|
5
|
+
from .core import Anchor, Shape, Bounds
|
|
6
6
|
from .base import Group
|
|
7
7
|
|
|
8
8
|
|
|
@@ -32,81 +32,60 @@ class Row(Layout):
|
|
|
32
32
|
self,
|
|
33
33
|
shapes: list[Shape] | None = None,
|
|
34
34
|
align: Align = "middle",
|
|
35
|
-
gap: float = 0,
|
|
35
|
+
gap: float = 0.0,
|
|
36
|
+
width: float | None = None,
|
|
37
|
+
mode: Literal["tight", "space-between", "space-around"] = "tight",
|
|
36
38
|
) -> None:
|
|
37
|
-
self.
|
|
39
|
+
self.align_mode = align
|
|
38
40
|
self.gap = gap
|
|
41
|
+
self.width = width
|
|
42
|
+
self.mode = mode
|
|
39
43
|
super().__init__(shapes)
|
|
40
44
|
|
|
41
45
|
def do_layout(self) -> None:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
# Map Row's 'align' terminology to the Group's 'anchor' terminology
|
|
47
|
+
anchor_map: dict[Align, Anchor] = {
|
|
48
|
+
"start": "top",
|
|
49
|
+
"middle": "center",
|
|
50
|
+
"end": "bottom",
|
|
51
|
+
}
|
|
48
52
|
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
# 1. Distribute along the flow axis (X)
|
|
54
|
+
self.distribute(
|
|
55
|
+
axis="horizontal", size=self.width, mode=self.mode, gap=self.gap
|
|
56
|
+
)
|
|
52
57
|
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
# 2. Align along the cross axis (Y)
|
|
59
|
+
self.align(axis="vertical", anchor=anchor_map[self.align_mode])
|
|
55
60
|
|
|
56
|
-
# Calculate Y based on baseline
|
|
57
|
-
match self.align:
|
|
58
|
-
case "start":
|
|
59
|
-
dy = -b.y
|
|
60
|
-
case "middle":
|
|
61
|
-
dy = (max_h / 2) - (b.y + b.height / 2)
|
|
62
|
-
case "end":
|
|
63
|
-
dy = max_h - (b.y + b.height)
|
|
64
|
-
case _:
|
|
65
|
-
dy = 0
|
|
66
61
|
|
|
67
|
-
|
|
68
|
-
shape.transform.tx = current_x - b.x
|
|
69
|
-
shape.transform.ty = dy
|
|
70
|
-
|
|
71
|
-
current_x += b.width + self.gap
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
class Column(Row):
|
|
62
|
+
class Column(Layout):
|
|
75
63
|
def __init__(
|
|
76
64
|
self,
|
|
77
65
|
shapes: list[Shape] | None = None,
|
|
78
66
|
align: Align = "middle",
|
|
79
|
-
gap: float = 0,
|
|
67
|
+
gap: float = 0.0,
|
|
68
|
+
height: float | None = None,
|
|
69
|
+
mode: Literal["tight", "space-between", "space-around"] = "tight",
|
|
80
70
|
) -> None:
|
|
81
|
-
|
|
71
|
+
self.align_mode = align
|
|
72
|
+
self.gap = gap
|
|
73
|
+
self.height = height
|
|
74
|
+
self.mode = mode
|
|
75
|
+
super().__init__(shapes)
|
|
82
76
|
|
|
83
77
|
def do_layout(self) -> None:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
max_w = max(s.local().width for s in self.shapes)
|
|
91
|
-
current_y = 0.0
|
|
92
|
-
|
|
93
|
-
for shape in self.shapes:
|
|
94
|
-
b = shape.local()
|
|
95
|
-
|
|
96
|
-
match self.align:
|
|
97
|
-
case "start":
|
|
98
|
-
dx = -b.x
|
|
99
|
-
case "end":
|
|
100
|
-
dx = max_w - (b.x + b.width)
|
|
101
|
-
case "middle":
|
|
102
|
-
dx = (max_w / 2) - (b.x + b.width / 2)
|
|
103
|
-
case _:
|
|
104
|
-
dx = 0
|
|
78
|
+
anchor_map: dict[Align, Anchor] = {
|
|
79
|
+
"start": "left",
|
|
80
|
+
"middle": "center",
|
|
81
|
+
"end": "right",
|
|
82
|
+
}
|
|
105
83
|
|
|
106
|
-
|
|
107
|
-
|
|
84
|
+
# 1. Distribute along the flow axis (Y)
|
|
85
|
+
self.distribute(axis="vertical", size=self.height, mode=self.mode, gap=self.gap)
|
|
108
86
|
|
|
109
|
-
|
|
87
|
+
# 2. Align along the cross axis (X)
|
|
88
|
+
self.align(axis="horizontal", anchor=anchor_map[self.align_mode])
|
|
110
89
|
|
|
111
90
|
|
|
112
91
|
class ForceLayout(Layout):
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
tesserax/__init__.py,sha256=r9-tqABA4B8owCxvcG1eUMAup6fqWqpTPVr2_JGg0Q0,224
|
|
2
|
+
tesserax/align.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
tesserax/base.py,sha256=rIQ4ihHCDxFsYcQRqzAxs6jbCsFeR8B8uJDi4eAHXOs,17695
|
|
4
|
+
tesserax/canvas.py,sha256=uY-L3DvIDwciDWQ1l9QLEH-unVUk8Am0k1ykeMIxqG0,3663
|
|
5
|
+
tesserax/core.py,sha256=7t_UxMl0cRJYspCIYPPUc0aXIdqcaHa_o01MrM_9IaY,6180
|
|
6
|
+
tesserax/layout.py,sha256=fPAycn4J3XT0J7L_BGy8Tmw58KoMjR5lQsiIdpkXIsY,11257
|
|
7
|
+
tesserax/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
tesserax-0.5.2.dist-info/METADATA,sha256=rcct3LcEqwxUVk14VjyK9NTeOFFJc4GVsPmANrwn6h0,7320
|
|
9
|
+
tesserax-0.5.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
10
|
+
tesserax-0.5.2.dist-info/licenses/LICENSE,sha256=MqaKRdqJfordIyistXMUlS7MVRj7fnJABGUD0Q3Fy14,1071
|
|
11
|
+
tesserax-0.5.2.dist-info/RECORD,,
|
tesserax-0.5.1.dist-info/RECORD
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
tesserax/__init__.py,sha256=XitPTo-_7JHiHKO9XipLgu_HnijDja0q_lmeTLfaUeQ,224
|
|
2
|
-
tesserax/align.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
tesserax/base.py,sha256=o7ZQ0ED7EPPCvas78OBq4fE5w38iWdMwl7WYxzFlWyw,14404
|
|
4
|
-
tesserax/canvas.py,sha256=uY-L3DvIDwciDWQ1l9QLEH-unVUk8Am0k1ykeMIxqG0,3663
|
|
5
|
-
tesserax/core.py,sha256=7t_UxMl0cRJYspCIYPPUc0aXIdqcaHa_o01MrM_9IaY,6180
|
|
6
|
-
tesserax/layout.py,sha256=C_enRYUUizCQpnQy_MK8hPbZUT7gJ7sa8l7mJwp_Kak,11591
|
|
7
|
-
tesserax/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
tesserax-0.5.1.dist-info/METADATA,sha256=8mfM0_-XroM93xd13EO6x_FAsmZnBNKD8RMIXBVR4CE,7320
|
|
9
|
-
tesserax-0.5.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
10
|
-
tesserax-0.5.1.dist-info/licenses/LICENSE,sha256=MqaKRdqJfordIyistXMUlS7MVRj7fnJABGUD0Q3Fy14,1071
|
|
11
|
-
tesserax-0.5.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|