manim-mindmap 0.1.0__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.
@@ -0,0 +1,3 @@
1
+ from .nodes import Node,NodeStyle,bfs_walker,dfs_walker
2
+ from .mindmap import MindMap
3
+ from .animations import *
@@ -0,0 +1 @@
1
+ from .tidy_tree import *
@@ -0,0 +1,348 @@
1
+ """
2
+ Non-layered Tidy Tree Layout Algorithm (Python Implementation)
3
+ 用于计算树形结构的节点位置布局
4
+
5
+ 算法参考来源《Improving Walker's Algorithm to Run in Linear Time》
6
+ """
7
+ __all__ = [
8
+ 'tidy_tree_layout'
9
+ ]
10
+ from dataclasses import dataclass, field
11
+ from typing import List, Optional, Any
12
+
13
+ @dataclass
14
+ class WrappedTree:
15
+ """包装树节点,用于布局计算"""
16
+ # 原始节点引用
17
+ node: Any = None
18
+ # 基本属性
19
+ x: float = 0.0
20
+ y: float = 0.0
21
+ width: float = 0.0
22
+ height: float = 0.0
23
+ # 子节点
24
+ children: List['WrappedTree'] = field(default_factory=list)
25
+ child_number: int = 0
26
+
27
+ # === 核心布局属性 ===
28
+ prelim: float = 0.0 # 节点相对于父节点的初步位置,不考虑任何重叠修正
29
+ mod: float = 0.0 # 以当前节点为根的整棵子树需要额外平移的量
30
+ shift: float = 0.0 # 记录需要均匀分摊到后续兄弟子树的间距份额
31
+ change: float = 0.0 # 在分摊链的末端节点做一次性修正,消除累积误差
32
+
33
+ # === 子树轮廓线追踪,用于不重叠检测 ===
34
+ extreme_left: Optional['WrappedTree'] = None # 最左端点节点
35
+ extreme_right: Optional['WrappedTree'] = None # 最右端点节点
36
+ mod_sum_extreme_left: float = 0.0 # 从最左端点到根路径上所有 mod 的累加和
37
+ mod_sum_extreme_right: float = 0.0 # 从最右端点到根路径上所有 mod 的累加和
38
+
39
+ # 线程,用于轮廓遍历: 两个子树的高度不同时,矮树耗尽时,链接到临近子树的轮廓
40
+ thread_left: Optional['WrappedTree'] = None
41
+ thread_right: Optional['WrappedTree'] = None
42
+
43
+ @classmethod
44
+ def from_node(cls, node, is_horizontal: bool) -> 'WrappedTree':
45
+ """从原始节点创建包装树"""
46
+ wt = cls()
47
+ wt.node = node
48
+
49
+ # 复制尺寸
50
+ if is_horizontal:
51
+ wt.width = getattr(node, 'height', 0)
52
+ wt.height = getattr(node, 'width', 0)
53
+ wt.x = getattr(node, 'x', 0)
54
+ else:
55
+ wt.width = getattr(node, 'width', 0)
56
+ wt.height = getattr(node, 'height', 0)
57
+ wt.y = getattr(node, 'y', 0)
58
+
59
+ # 递归创建子节点
60
+ children = getattr(node, 'children', [])
61
+ wt.children = [cls.from_node(child, is_horizontal) for child in children]
62
+ wt.child_number = len(wt.children)
63
+ return wt
64
+
65
+ @dataclass
66
+ class IYLNode:
67
+ """
68
+ separate 函数中的最低轮廓线:
69
+ 为避免第 i 个子树与前面 i-1 个子树的逐一比较,通过链表单调裁剪,提升效率!
70
+ 被 IYLNode 淘汰的子树,已被更高且更靠右的中间子树完全遮挡,当前子树永远不可能先撞到它们
71
+ """
72
+ low: float # 子树最右轮廓低端节点,在正交方向上的高度
73
+ index: int # 子树在兄弟节点中的索引
74
+ nxt: Optional['IYLNode'] = None # 指向链表中下一个 IYL 节点(该节点的 low 值严格更高)
75
+
76
+ def move_right(node, move: float, is_horizontal: bool):
77
+ """向右(或向下)移动节点及其所有子节点"""
78
+ if is_horizontal:
79
+ node.y += move
80
+ else:
81
+ node.x += move
82
+ for child in node.children:
83
+ move_right(child, move, is_horizontal)
84
+
85
+ def get_min(node, is_horizontal: bool) -> float:
86
+ """获取节点树中最小坐标值"""
87
+ res = node.y if is_horizontal else node.x
88
+ for child in node.children:
89
+ res = min(get_min(child, is_horizontal), res)
90
+ return res
91
+
92
+ def normalize(node, is_horizontal: bool):
93
+ """归一化坐标: 将最小坐标对齐到0,确保布局从原点开始"""
94
+ min_val = get_min(node, is_horizontal)
95
+ move_right(node, -min_val, is_horizontal)
96
+
97
+ def convert_back(converted: WrappedTree, root, is_horizontal: bool):
98
+ """将计算结果转换回原始节点:将 WrappedTree.x 写回原始节点的 x 或 y(根据方向)"""
99
+ if is_horizontal:
100
+ root.y = converted.x
101
+ else:
102
+ root.x = converted.x
103
+
104
+ for i, child in enumerate(converted.children):
105
+ if i < len(root.children):
106
+ convert_back(child, root.children[i], is_horizontal)
107
+
108
+ def layer(node, direction,level_spacing):
109
+ """设置层级(深度)坐标"""
110
+ if (parent := node.parent) is not None:
111
+ node.level = parent.level + 1
112
+ if direction == 'right':
113
+ node.x = parent.x + (parent.width + node.width) / 2 + level_spacing
114
+ elif direction == 'left':
115
+ node.x = parent.x - (parent.width + node.width) / 2 - level_spacing
116
+ elif direction == 'up':
117
+ node.y = parent.y + (parent.height + node.height) / 2 + level_spacing
118
+ else:
119
+ node.y = parent.y - (parent.height + node.height) / 2 - level_spacing
120
+
121
+ for child in node.children:
122
+ layer(child, direction,level_spacing)
123
+
124
+ class TidyTreeLayout:
125
+ """非分层整洁树布局算法"""
126
+ def __init__(
127
+ self,
128
+ root,
129
+ direction: str = 'right',
130
+ node_spacing: float = 0.5,
131
+ level_spacing: float = 0.5
132
+ ):
133
+ self.root = root
134
+ self.direction = direction
135
+ self.is_horizontal = self._is_horizontal(direction)
136
+ self.node_spacing = node_spacing
137
+ self.level_spacing = level_spacing
138
+ self.wt = None
139
+
140
+ def _is_horizontal(self,direction):
141
+ return direction in ('right', 'left')
142
+
143
+ def layout(self):
144
+ """执行布局计算"""
145
+ layer(self.root, self.direction,self.level_spacing)
146
+ self.wt = WrappedTree.from_node(self.root, self.is_horizontal)
147
+ self.first_walk(self.wt)
148
+ self.second_walk(self.wt, 0)
149
+ convert_back(self.wt, self.root, self.is_horizontal)
150
+ normalize(self.root, self.is_horizontal)
151
+ return self.root
152
+
153
+ def first_walk(self, t: WrappedTree):
154
+ """
155
+ 第一次遍历:
156
+ 计算每个节点的 prelim(初步相对位置)和 mod(修饰偏移),检测并消除子树间的重叠
157
+ """
158
+ if t.child_number == 0:
159
+ self.set_extremes(t)
160
+ return
161
+
162
+ # 处理第一个子节点
163
+ self.first_walk(t.children[0])
164
+ ih = self.update_iyl(self.bottom(t.children[0].extreme_right), 0, None)
165
+
166
+ # 处理其余子节点
167
+ for i in range(1, t.child_number):
168
+ self.first_walk(t.children[i])
169
+ min_val = self.bottom(t.children[i].extreme_right)
170
+ self.separate(t, i, ih)
171
+ ih = self.update_iyl(min_val, i, ih)
172
+
173
+ self.position_root(t)
174
+ self.set_extremes(t)
175
+
176
+ def set_extremes(self, t: WrappedTree):
177
+ """设置极值节点"""
178
+ if t.child_number == 0:
179
+ t.extreme_left = t
180
+ t.extreme_right = t
181
+ t.mod_sum_extreme_left = 0
182
+ t.mod_sum_extreme_right = 0
183
+ else:
184
+ t.extreme_left = t.children[0].extreme_left
185
+ t.mod_sum_extreme_left = t.children[0].mod_sum_extreme_left
186
+ t.extreme_right = t.children[-1].extreme_right
187
+ t.mod_sum_extreme_right = t.children[-1].mod_sum_extreme_right
188
+
189
+ def update_iyl(self, low: float, index: int, ih: Optional[IYLNode]) -> IYLNode:
190
+ """
191
+ 更新 IYLNode 链表: 淘汰掉 ih.low 值小于等于 low 的节点
192
+ 返回: 新的节点成为表头,并指向剩余链表
193
+ """
194
+ while ih is not None and low >= ih.low:
195
+ ih = ih.nxt
196
+ return IYLNode(low, index, ih)
197
+
198
+ def separate(self, t: WrappedTree, i: int, ih: Optional[IYLNode]):
199
+ """
200
+ 算法核心: 分离第i个子树与前面子树,确保不重叠
201
+
202
+ 参数:
203
+ t (WrappedTree): 父节点
204
+ i (int): 当前子树索引
205
+ ih (Optional[IYLNode]): 那些"右轮廓还暴露在外、可能先与第 i 个子树发生碰撞"的前序子树
206
+ """
207
+ sr = t.children[i - 1] # 当前右轮廓节点,初始为紧邻的左兄弟
208
+ mssr = sr.mod # 从 t.children[i-1] 到 sr 路径上的 mod 累加和
209
+ cl = t.children[i] # 当前左轮廓节点,初始为当前子树根节点
210
+ mscl = cl.mod # 从 t.children[i] 到 cl 路径上的 mod 累加和
211
+
212
+ while sr is not None and cl is not None:
213
+ # 跳过已被完全遮挡的前序子树
214
+ if ih is not None and self.bottom(sr) > ih.low:
215
+ # 当前 sr 高度已经超过了该前序子树的轮廓范围,在这个高度上,ih 不可能与当前子树碰撞
216
+ # 跳过该前序子树,继续与下一个更高的前序子树比较
217
+ ih = ih.nxt
218
+
219
+ dist = mssr + sr.prelim + self.node_spacing + (sr.width + cl.width)/2 - (mscl + cl.prelim)
220
+ if dist > 0:
221
+ mscl += dist
222
+ si = ih.index if ih is not None else i - 1
223
+ self.move_subtree(t, i, si, dist)
224
+
225
+ sy = self.bottom(sr)
226
+ cy = self.bottom(cl)
227
+
228
+ if sy <= cy:
229
+ sr = self.next_right_contour(sr)
230
+ if sr is not None:
231
+ mssr += sr.mod
232
+
233
+ if sy >= cy:
234
+ cl = self.next_left_contour(cl)
235
+ if cl is not None:
236
+ mscl += cl.mod
237
+
238
+ if sr is None and cl is not None:
239
+ self.set_left_thread(t, i, cl, mscl)
240
+ elif sr is not None and cl is None:
241
+ self.set_right_thread(t, i, sr, mssr)
242
+
243
+ def move_subtree(self, t: WrappedTree, i: int, si: int, dist: float):
244
+ """移动子树"""
245
+ t.children[i].mod += dist
246
+ t.children[i].mod_sum_extreme_left += dist
247
+ t.children[i].mod_sum_extreme_right += dist
248
+ self.distribute_extra(t, i, si, dist)
249
+
250
+ def distribute_extra(self, t: WrappedTree, i: int, si: int, dist: float):
251
+ """分配额外间距"""
252
+ if si != i - 1:
253
+ nr = i - si
254
+ t.children[si + 1].shift += dist / nr
255
+ t.children[i].shift -= dist / nr
256
+ t.children[i].change -= dist - dist / nr
257
+
258
+ def next_left_contour(self, t: WrappedTree) -> Optional[WrappedTree]:
259
+ """获取下一个左轮廓节点"""
260
+ return t.thread_left if t.child_number == 0 else t.children[0]
261
+
262
+ def next_right_contour(self, t: WrappedTree) -> Optional[WrappedTree]:
263
+ """获取下一个右轮廓节点"""
264
+ return t.thread_right if t.child_number == 0 else t.children[-1]
265
+
266
+ def bottom(self, t: WrappedTree) -> float:
267
+ """返回节点在正交方向上的高度"""
268
+ if self.is_horizontal:
269
+ return t.height/2 + abs(t.x)
270
+ return abs(t.y) + t.height/2
271
+
272
+ def set_left_thread(self, t: WrappedTree, i: int, cl: WrappedTree, modsumcl: float):
273
+ """设置左线程"""
274
+ li = t.children[0].extreme_left
275
+ li.thread_left = cl
276
+ diff = (modsumcl - cl.mod) - t.children[0].mod_sum_extreme_left
277
+ li.mod += diff
278
+ li.prelim -= diff
279
+ t.children[0].extreme_left = t.children[i].extreme_left
280
+ t.children[0].mod_sum_extreme_left = t.children[i].mod_sum_extreme_left
281
+
282
+ def set_right_thread(self, t: WrappedTree, i: int, sr: WrappedTree, modsumsr: float):
283
+ """设置右线程"""
284
+ ri = t.children[i].extreme_right
285
+ ri.thread_right = sr
286
+ diff = (modsumsr - sr.mod) - t.children[i].mod_sum_extreme_right
287
+ ri.mod += diff
288
+ ri.prelim -= diff
289
+ t.children[i].extreme_right = t.children[i - 1].extreme_right
290
+ t.children[i].mod_sum_extreme_right = t.children[i - 1].mod_sum_extreme_right
291
+
292
+ def position_root(self, t: WrappedTree):
293
+ """放置子树 t 根节点: 将其放置在子节点的正中间"""
294
+ t.prelim = (
295
+ t.children[0].prelim + t.children[0].mod + t.children[-1].mod + t.children[-1].prelim
296
+ ) / 2
297
+
298
+ def second_walk(self, t: WrappedTree, modsum: float):
299
+ """
300
+ 第二次遍历:
301
+ 将 prelim 和 mod 累加到最终绝对非层级的坐标 x 或 y 上
302
+
303
+ 参数:
304
+ modsum: 从根到当前路径上所有祖先 mod 值的总和
305
+ """
306
+ modsum += t.mod
307
+ if self.is_horizontal:
308
+ t.x = -t.prelim - modsum
309
+ else:
310
+ t.x = t.prelim + modsum
311
+ self.add_child_spacing(t)
312
+
313
+ for child in t.children:
314
+ self.second_walk(child, modsum)
315
+
316
+ def add_child_spacing(self, t: WrappedTree):
317
+ """添加子节点间距"""
318
+ d = 0
319
+ modsumdelta = 0
320
+ for child in t.children:
321
+ d += child.shift
322
+ modsumdelta += d + child.change
323
+ child.mod += modsumdelta
324
+
325
+ def tidy_tree_layout(
326
+ root,
327
+ direction: str = "down",
328
+ node_spacing: float = 0.5,
329
+ level_spacing: float = 0.5
330
+ ):
331
+ """
332
+ 对树形结构进行整洁布局函数接口
333
+
334
+ Args:
335
+ root: 根节点,需要包含以下属性:
336
+ - width: 节点宽度
337
+ - height: 节点高度
338
+ - children: 子节点列表
339
+ - x, y: 布局后的坐标(会被修改)
340
+ direction: 布局方向,可选 "down", "up", "left", "right",默认 "down"
341
+ node_spacing: 兄弟节点间距,默认0.5
342
+ level_spacing: 层级节点间距,默认0.5
343
+
344
+ Returns:
345
+ root: 修改后的根节点(x, y 已被设置)
346
+ """
347
+ layout = TidyTreeLayout(root, direction,node_spacing, level_spacing)
348
+ return layout.layout()
@@ -0,0 +1 @@
1
+ from .animations import *