kotonebot 0.4.0__py3-none-any.whl → 0.6.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.
Files changed (71) hide show
  1. kotonebot/backend/context/context.py +1002 -1002
  2. kotonebot/backend/core.py +6 -49
  3. kotonebot/backend/image.py +36 -5
  4. kotonebot/backend/loop.py +222 -208
  5. kotonebot/backend/ocr.py +7 -1
  6. kotonebot/client/device.py +108 -243
  7. kotonebot/client/host/__init__.py +34 -3
  8. kotonebot/client/host/adb_common.py +7 -9
  9. kotonebot/client/host/custom.py +6 -2
  10. kotonebot/client/host/leidian_host.py +2 -7
  11. kotonebot/client/host/mumu12_host.py +2 -7
  12. kotonebot/client/host/protocol.py +4 -3
  13. kotonebot/client/implements/__init__.py +62 -11
  14. kotonebot/client/implements/adb.py +5 -1
  15. kotonebot/client/implements/nemu_ipc/__init__.py +4 -0
  16. kotonebot/client/implements/uiautomator2.py +6 -2
  17. kotonebot/client/implements/windows.py +7 -3
  18. kotonebot/client/registration.py +1 -1
  19. kotonebot/client/scaler.py +467 -0
  20. kotonebot/config/base_config.py +1 -1
  21. kotonebot/config/config.py +61 -0
  22. kotonebot/core/__init__.py +13 -0
  23. kotonebot/core/entities/base.py +182 -0
  24. kotonebot/core/entities/compound.py +75 -0
  25. kotonebot/core/entities/ocr.py +117 -0
  26. kotonebot/core/entities/template_match.py +198 -0
  27. kotonebot/devtools/__init__.py +42 -0
  28. kotonebot/devtools/cli/__init__.py +6 -0
  29. kotonebot/devtools/cli/main.py +53 -0
  30. kotonebot/devtools/project/project.py +41 -0
  31. kotonebot/devtools/project/scanner.py +202 -0
  32. kotonebot/devtools/project/schema.py +99 -0
  33. kotonebot/devtools/resgen/__init__.py +42 -0
  34. kotonebot/devtools/resgen/codegen.py +331 -0
  35. kotonebot/devtools/resgen/core.py +94 -0
  36. kotonebot/devtools/resgen/parsers.py +360 -0
  37. kotonebot/devtools/resgen/utils.py +158 -0
  38. kotonebot/devtools/resgen/validation.py +115 -0
  39. kotonebot/devtools/web/dist/assets/bootstrap-icons-BOrJxbIo.woff +0 -0
  40. kotonebot/devtools/web/dist/assets/bootstrap-icons-BtvjY1KL.woff2 +0 -0
  41. kotonebot/devtools/web/dist/assets/ext-language_tools-CD021WJ2.js +2577 -0
  42. kotonebot/devtools/web/dist/assets/index-B_m5f2LF.js +2836 -0
  43. kotonebot/devtools/web/dist/assets/index-BlEDyGGa.css +9 -0
  44. kotonebot/devtools/web/dist/assets/language-client-C9muzqaq.js +128 -0
  45. kotonebot/devtools/web/dist/assets/mode-python-CtHp76XS.js +476 -0
  46. kotonebot/devtools/web/dist/icons/symbol-class.svg +3 -0
  47. kotonebot/devtools/web/dist/icons/symbol-file.svg +3 -0
  48. kotonebot/devtools/web/dist/icons/symbol-method.svg +3 -0
  49. kotonebot/devtools/web/dist/index.html +25 -0
  50. kotonebot/devtools/web/server/__init__.py +0 -0
  51. kotonebot/devtools/web/server/rest_api.py +217 -0
  52. kotonebot/devtools/web/server/server.py +85 -0
  53. kotonebot/errors.py +7 -2
  54. kotonebot/interop/win/__init__.py +10 -1
  55. kotonebot/interop/win/_mouse.py +311 -0
  56. kotonebot/interop/win/shake_mouse.py +224 -0
  57. kotonebot/primitives/__init__.py +3 -1
  58. kotonebot/primitives/geometry.py +817 -40
  59. kotonebot/primitives/visual.py +81 -1
  60. kotonebot/ui/pushkit/image_host.py +2 -1
  61. kotonebot/ui/pushkit/wxpusher.py +2 -1
  62. {kotonebot-0.4.0.dist-info → kotonebot-0.6.0.dist-info}/METADATA +4 -1
  63. kotonebot-0.6.0.dist-info/RECORD +105 -0
  64. kotonebot-0.6.0.dist-info/entry_points.txt +2 -0
  65. kotonebot/client/implements/adb_raw.py +0 -159
  66. kotonebot-0.4.0.dist-info/RECORD +0 -70
  67. /kotonebot/{tools → devtools}/mirror.py +0 -0
  68. /kotonebot/{tools → devtools/project}/__init__.py +0 -0
  69. {kotonebot-0.4.0.dist-info → kotonebot-0.6.0.dist-info}/WHEEL +0 -0
  70. {kotonebot-0.4.0.dist-info → kotonebot-0.6.0.dist-info}/licenses/LICENSE +0 -0
  71. {kotonebot-0.4.0.dist-info → kotonebot-0.6.0.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,81 @@
1
- from typing import Generic, TypeVar, TypeGuard, overload
1
+ """
2
+ # 几何基础模块 (geometry)
3
+
4
+ 此模块提供了用于处理几何图形和坐标系统的基础类,包括点、向量、矩形等几何对象的表示和操作。
5
+
6
+ ## 主要功能
7
+
8
+ - **坐标系统**:提供二维、三维、四维坐标的表示
9
+ - **点操作**:支持整数和浮点数坐标点的各种运算
10
+ - **矩形操作**:提供矩形的创建、变换、相交检测等功能
11
+ - **向量运算**:支持向量的加减乘除、距离计算、单位化等操作
12
+ - **类型安全**:使用泛型和类型守卫确保类型安全
13
+
14
+ ## 核心类
15
+
16
+ - `Vector2D` - 二维向量,支持泛型
17
+ - `Vector3D` - 三维向量,适用于颜色和空间坐标
18
+ - `Vector4D` - 四维向量,适用于 RGBA 等场景
19
+ - `Point` - 整数坐标点,适用于像素定位
20
+ - `PointF` - 浮点数坐标点,适用于精确计算
21
+ - `Rect` - 矩形类,提供丰富的几何操作
22
+
23
+ ## 使用示例
24
+
25
+ ```python
26
+ # 坐标点操作
27
+ p1 = Point(100, 200, name="起始点")
28
+ p2 = PointF(150.5, 250.8)
29
+ distance = p1.distance_to(p2)
30
+
31
+ # 矩形操作
32
+ rect = Rect(10, 20, 100, 50, name="按钮区域")
33
+ center = rect.center
34
+ enlarged = rect.inflate(5, 5)
35
+
36
+ # 向量运算
37
+ v1 = Vector2D(10, 20)
38
+ v2 = Vector2D(5, 8)
39
+ result = v1 + v2
40
+ ```
41
+
42
+ ## 注意事项
43
+
44
+ - 所有坐标系统遵循屏幕坐标系,原点在左上角
45
+ - 矩形操作使用半开区间规则:[x1, x2) x [y1, y2)
46
+ - 点和向量的运算会根据输入类型自动选择返回类型
47
+ """
48
+
49
+ import math
50
+ from typing import Generic, TypeVar, TypeGuard, overload, Union
2
51
 
3
52
  T = TypeVar('T')
4
53
 
5
54
  class Vector2D(Generic[T]):
6
- """2D 坐标类"""
55
+ """
56
+ ## Vector2D
57
+ 表示一个二维向量。此类支持泛型,可用于表示不同类型的坐标(如整数、浮点数等)。
58
+
59
+ ### 例
60
+ ```python
61
+ # 创建二维坐标
62
+ v = Vector2D(10, 20, name="位置")
63
+ print(v.x, v.y) # 10, 20
64
+
65
+ # 使用索引访问
66
+ print(v[0], v[1]) # 10, 20
67
+
68
+ # 坐标计算
69
+ v2 = Vector2D(5, 8)
70
+ print(f"坐标距离: {math.sqrt((v.x - v2.x)**2 + (v.y - v2.y)**2):.2f}")
71
+
72
+ # 支持命名坐标
73
+ center = Vector2D(640, 360, name="屏幕中心")
74
+ print(center) # Point<"屏幕中心" at (640, 360)>
75
+ ```
76
+ """
77
+ __slots__ = ('x', 'y', 'name')
78
+
7
79
  def __init__(self, x: T, y: T, *, name: str | None = None):
8
80
  self.x = x
9
81
  self.y = y
@@ -18,6 +90,14 @@ class Vector2D(Generic[T]):
18
90
  else:
19
91
  raise IndexError
20
92
 
93
+ def __iter__(self):
94
+ yield self.x
95
+ yield self.y
96
+
97
+ def as_tuple(self) -> tuple[T, T]:
98
+ """Return coordinates as a tuple of ints: (x, y)."""
99
+ return self.x, self.y
100
+
21
101
  def __repr__(self) -> str:
22
102
  return f'Point<"{self.name}" at ({self.x}, {self.y})>'
23
103
 
@@ -26,7 +106,30 @@ class Vector2D(Generic[T]):
26
106
 
27
107
 
28
108
  class Vector3D(Generic[T]):
29
- """三元组类。"""
109
+ """
110
+ ## Vector3D
111
+ 表示一个三维向量。
112
+
113
+ ### 例
114
+ ```python
115
+ # 创建三维坐标
116
+ v3 = Vector3D(100, 200, 50, name="3D点")
117
+ print(v3.x, v3.y, v3.z) # 100, 200, 50
118
+
119
+ # 使用索引访问
120
+ print(v3[0], v3[1], v3[2]) # 100, 200, 50
121
+
122
+ # 解构
123
+ x, y, z = v3.xyz # (100, 200, 50)
124
+ x, y = v3.xy # (100, 200)
125
+
126
+ # 颜色值应用
127
+ rgb = Vector3D(255, 128, 64, name="颜色")
128
+ print(f"RGB: {rgb.rgb if hasattr(v3, 'rgb') else '未定义'}")
129
+ ```
130
+ """
131
+ __slots__ = ('x', 'y', 'z', 'name')
132
+
30
133
  def __init__(self, x: T, y: T, z: T, *, name: str | None = None):
31
134
  self.x = x
32
135
  self.y = y
@@ -44,6 +147,15 @@ class Vector3D(Generic[T]):
44
147
  else:
45
148
  raise IndexError
46
149
 
150
+ def __iter__(self):
151
+ yield self.x
152
+ yield self.y
153
+ yield self.z
154
+
155
+ def as_tuple(self) -> tuple[T, T, T]:
156
+ """Return coordinates as a tuple of ints: (x, y, z)."""
157
+ return self.x, self.y, self.z
158
+
47
159
  @property
48
160
  def xyz(self) -> tuple[T, T, T]:
49
161
  """
@@ -59,7 +171,30 @@ class Vector3D(Generic[T]):
59
171
  return self.x, self.y
60
172
 
61
173
  class Vector4D(Generic[T]):
62
- """四元组类。"""
174
+ """
175
+ ## Vector4D
176
+ 此类用于表示四维坐标或向量,通常用于颜色空间(如RGBA)等场景。
177
+
178
+ ### 例
179
+ ```python
180
+ # 创建四维坐标
181
+ v4 = Vector4D(100, 200, 150, 255, name="颜色值")
182
+ print(f"RGBA: {v4.x}, {v4.y}, {v4.z}, {v4.w}") # 100, 200, 150, 255
183
+
184
+ # 使用索引访问
185
+ print(v4[0], v4[1], v4[2], v4[3]) # 100, 200, 150, 255
186
+
187
+ # 在颜色空间中使用
188
+ color = Vector4D(255, 0, 0, 128, name="半透明红色")
189
+ print(f"颜色: {color}")
190
+
191
+ # 四维向量运算
192
+ v4_2 = Vector4D(50, 50, 50, 100)
193
+ # 注意:Vector4D 未定义运算,但可用于数据存储
194
+ ```
195
+ """
196
+ __slots__ = ('x', 'y', 'z', 'w', 'name')
197
+
63
198
  def __init__(self, x: T, y: T, z: T, w: T, *, name: str | None = None):
64
199
  self.x = x
65
200
  self.y = y
@@ -80,61 +215,312 @@ class Vector4D(Generic[T]):
80
215
  else:
81
216
  raise IndexError
82
217
 
83
- Size = Vector2D[int]
84
- """尺寸。相当于 Vector2D[int]"""
218
+ def __iter__(self):
219
+ yield self.x
220
+ yield self.y
221
+ yield self.z
222
+ yield self.w
223
+
224
+ def as_tuple(self) -> tuple[T, T, T, T]:
225
+ """Return coordinates as a tuple of ints: (x, y, z, w)."""
226
+ return self.x, self.y, self.z, self.w
227
+
85
228
  RectTuple = tuple[int, int, int, int]
86
229
  """矩形。(x, y, w, h)"""
87
230
  PointTuple = tuple[int, int]
88
231
  """点。(x, y)"""
232
+ PointFTuple = tuple[float, float]
233
+ """浮点数点。(x, y)"""
234
+ Size = Vector2D[int]
235
+ """尺寸。相当于 Vector2D[int]"""
236
+ SizeTuple = tuple[int, int]
237
+ """尺寸。(width, height)"""
238
+ SizeLike = Union[Size, SizeTuple]
239
+ """尺寸类型,可以是 Size 对象或尺寸元组。"""
240
+
89
241
 
90
- class Point(Vector2D[int]):
91
- """点。"""
242
+ Number = TypeVar('Number', int, float)
243
+ """数字类型,可以是整数或浮点数。"""
244
+ NumberTuple2D = tuple[Number, Number]
245
+ class _BasePoint(Vector2D[Number]):
246
+ __slots__ = (*Vector2D.__slots__,)
92
247
 
93
248
  @property
94
- def xy(self) -> PointTuple:
249
+ def xy(self) -> NumberTuple2D:
95
250
  """
96
- 二元组 (x, y)。OpenCV 格式的坐标。
251
+ 二元组 (x, y)。
252
+
253
+ :return: 包含 x 和 y 坐标的元组。
97
254
  """
98
255
  return self.x, self.y
99
-
100
- def offset(self, dx: int, dy: int) -> 'Point':
256
+
257
+ @property
258
+ def length(self) -> float:
101
259
  """
102
- 偏移坐标。
260
+ 将点视为从原点出发的向量,其长度(模)。
261
+
262
+ :return: 向量长度。
263
+ """
264
+ return math.sqrt(self.x**2 + self.y**2)
265
+
266
+ def distance_to(self, other: 'AnyPoint | AnyPointTuple') -> float:
267
+ """
268
+ 计算到另一个点的距离。
103
269
 
104
- :param dx: 偏移量。
105
- :param dy: 偏移量。
106
- :return: 偏移后的坐标。
270
+ :param other: 另一个点或元组。
271
+ :return: 距离。
107
272
  """
108
- return Point(self.x + dx, self.y + dy, name=self.name)
109
-
110
- def __add__(self, other: 'Point | PointTuple') -> 'Point':
273
+ return math.sqrt((self.x - other[0])**2 + (self.y - other[1])**2)
274
+
275
+ def __eq__(self, value: object) -> bool:
111
276
  """
112
- 相加。
277
+ 比较两个点是否相等。
113
278
 
114
- :param other: 另一个 Point 对象或二元组 (x: int, y: int)。
115
- :return: 相加后的点。
279
+ :param value: 另一个点。
280
+ :return: 如果坐标相等则返回 True,否则返回 False。
281
+ """
282
+ if isinstance(value, _BasePoint):
283
+ return self.x == value.x and self.y == value.y
284
+ return False
285
+
286
+ def __lt__(self, other: 'AnyPoint | AnyPointTuple') -> bool:
116
287
  """
117
- if isinstance(other, Point):
118
- return Point(self.x + other.x, self.y + other.y, name=self.name)
288
+ 小于比较,按 (x, y) 的字典序进行比较。
289
+ 支持 `Point` / `PointF` 或长度为2的元组/列表。
290
+ """
291
+ if is_any_point(other):
292
+ ox, oy = other.x, other.y
293
+ elif isinstance(other, tuple) and len(other) == 2:
294
+ ox, oy = other[0], other[1]
295
+ else:
296
+ return NotImplemented
297
+ return (self.x, self.y) < (ox, oy)
298
+
299
+ def __le__(self, other: 'AnyPoint | AnyPointTuple') -> bool:
300
+ """小于等于比较,按 (x, y) 的字典序比较。"""
301
+ if is_any_point(other):
302
+ ox, oy = other.x, other.y
303
+ elif isinstance(other, tuple) and len(other) == 2:
304
+ ox, oy = other[0], other[1]
119
305
  else:
120
- return Point(self.x + other[0], self.y + other[1], name=self.name)
306
+ return NotImplemented
307
+ return (self.x, self.y) <= (ox, oy)
308
+
309
+ def __gt__(self, other: 'AnyPoint | AnyPointTuple') -> bool:
310
+ """大于比较,按 (x, y) 的字典序比较。"""
311
+ if is_any_point(other):
312
+ ox, oy = other.x, other.y
313
+ elif isinstance(other, tuple) and len(other) == 2:
314
+ ox, oy = other[0], other[1]
315
+ else:
316
+ return NotImplemented
317
+ return (self.x, self.y) > (ox, oy)
318
+
319
+ def __ge__(self, other: 'AnyPoint | AnyPointTuple') -> bool:
320
+ """大于等于比较,按 (x, y) 的字典序比较。"""
321
+ if is_any_point(other):
322
+ ox, oy = other.x, other.y
323
+ elif isinstance(other, tuple) and len(other) == 2:
324
+ ox, oy = other[0], other[1]
325
+ else:
326
+ return NotImplemented
327
+ return (self.x, self.y) >= (ox, oy)
328
+
329
+ def normalized(self) -> 'PointF':
330
+ """
331
+ 返回一个新的、方向相同但长度为 1 的 `PointF` 对象(单位向量)。
332
+
333
+ :return: 单位向量。
334
+ """
335
+ l = self.length
336
+ if l == 0:
337
+ return PointF(0.0, 0.0, name=self.name)
338
+ return PointF(self.x / l, self.y / l, name=self.name)
339
+
340
+ def offset(self, dx: int | float, dy: int | float) -> 'AnyPoint':
341
+ """
342
+ 偏移坐标。
343
+
344
+ 如果 self, dx, dy 均为整数,返回 Point。否则返回 PointF。
345
+
346
+ :param dx: x方向偏移量。
347
+ :param dy: y方向偏移量。
348
+ :return: 偏移后的新 Point 或 PointF 对象。
349
+ """
350
+ new_x = self.x + dx
351
+ new_y = self.y + dy
352
+ if isinstance(self, PointF) or isinstance(dx, float) or isinstance(dy, float):
353
+ return PointF(new_x, new_y, name=self.name)
354
+ return Point(int(new_x), int(new_y), name=self.name)
355
+
356
+ @overload
357
+ def __add__(self: 'Point', other: 'Point | PointTuple') -> 'Point': ...
358
+ @overload
359
+ def __add__(self, other: 'PointF | PointFTuple') -> 'PointF': ...
360
+ @overload
361
+ def __add__(self: 'PointF', other: 'AnyPoint | AnyPointTuple') -> 'PointF': ...
362
+ def __add__(self, other: 'AnyPoint | AnyPointTuple') -> 'AnyPoint':
363
+ """
364
+ 与另一个点或元组相加。
365
+
366
+ 如果任一操作数为浮点数,则结果提升为 PointF。
121
367
 
122
- def __sub__(self, other: 'Point | PointTuple') -> 'Point':
368
+ :param other: 另一个 Point/PointF 对象或元组。
369
+ :return: 相加后的新 Point 或 PointF 对象。
370
+ """
371
+ new_x = self.x + other[0]
372
+ new_y = self.y + other[1]
373
+ # hasattr check for tuple, which does not have .x attribute
374
+ if isinstance(self, PointF) or isinstance(other, PointF) or \
375
+ (not hasattr(other, 'x') and (isinstance(other[0], float) or isinstance(other[1], float))):
376
+ return PointF(new_x, new_y, name=self.name)
377
+ return Point(int(new_x), int(new_y), name=self.name)
378
+
379
+ @overload
380
+ def __sub__(self: 'Point', other: 'Point | PointTuple') -> 'Point': ...
381
+ @overload
382
+ def __sub__(self, other: 'PointF | PointFTuple') -> 'PointF': ...
383
+ @overload
384
+ def __sub__(self: 'PointF', other: 'AnyPoint | AnyPointTuple') -> 'PointF': ...
385
+ def __sub__(self, other: 'AnyPoint | AnyPointTuple') -> 'AnyPoint':
123
386
  """
124
- 相减。
387
+ 与另一个点或元组相减。
388
+
389
+ 如果任一操作数为浮点数,则结果提升为 PointF。
125
390
 
126
- :param other: 另一个 Point 对象或二元组 (x: int, y: int)。
127
- :return: 相减后的点。
391
+ :param other: 另一个 Point/PointF 对象或元组。
392
+ :return: 相减后的新 Point 或 PointF 对象。
128
393
  """
129
- if isinstance(other, Point):
130
- return Point(self.x - other.x, self.y - other.y, name=self.name)
131
- else:
132
- return Point(self.x - other[0], self.y - other[1], name=self.name)
394
+ new_x = self.x - other[0]
395
+ new_y = self.y - other[1]
396
+ if isinstance(self, PointF) or isinstance(other, PointF) or \
397
+ (not hasattr(other, 'x') and (isinstance(other[0], float) or isinstance(other[1], float))):
398
+ return PointF(new_x, new_y, name=self.name)
399
+ return Point(int(new_x), int(new_y), name=self.name)
400
+
401
+ @overload
402
+ def __mul__(self: 'Point', scalar: int) -> 'Point': ...
403
+ @overload
404
+ def __mul__(self, scalar: float) -> 'PointF': ...
405
+ def __mul__(self, scalar: int | float) -> 'AnyPoint':
406
+ """
407
+ 与标量相乘(缩放)。
408
+
409
+ :param scalar: 用于缩放的标量。
410
+ :return: 缩放后的新 Point 或 PointF 对象。
411
+ """
412
+ new_x = self.x * scalar
413
+ new_y = self.y * scalar
414
+ if isinstance(self, PointF) or isinstance(scalar, float):
415
+ return PointF(new_x, new_y, name=self.name)
416
+ return Point(int(new_x), int(new_y), name=self.name)
417
+
418
+ def __truediv__(self, scalar: int | float) -> 'PointF':
419
+ """
420
+ 与标量相除(缩放)。总是返回一个 PointF 对象。
421
+
422
+ :param scalar: 用于缩放的标量。
423
+ :return: 缩放后的新 PointF 对象。
424
+ """
425
+ if scalar == 0:
426
+ raise ValueError("Cannot divide by zero")
427
+ return PointF(self.x / scalar, self.y / scalar, name=self.name)
428
+
429
+ class PointF(_BasePoint[float]):
430
+ """
431
+ ## PointF
432
+ 表示浮点数坐标点。
433
+
434
+ ### 例
435
+ ```python
436
+ # 创建浮点数坐标点
437
+ p1 = PointF(10.5, 20.7, name="精确位置")
438
+ p2 = PointF(5.2, 8.9)
439
+
440
+ # 距离计算
441
+ distance = p1.distance_to(p2) # 计算到另一点的距离
442
+ print(f"距离: {distance:.2f}")
443
+
444
+ # 向量运算
445
+ result = p1 + p2 # 坐标相加
446
+ scaled = p1 * 2 # 坐标缩放
447
+
448
+ # 单位向量
449
+ unit = p1.normalized() # 转换为单位向量
450
+
451
+ # 坐标偏移
452
+ moved = p1.offset(5.0, 10.0) # 偏移坐标
453
+ ```
454
+ """
455
+ __slots__ = (*_BasePoint.__slots__,)
456
+
457
+
458
+ class Point(_BasePoint[int]):
459
+ """
460
+ ## Point
461
+ 表示整数坐标点。
462
+
463
+ ### 例
464
+ ```python
465
+ # 创建整数坐标点
466
+ pixel = Point(1920, 1080, name="屏幕分辨率")
467
+ button = Point(100, 200, name="按钮位置")
468
+
469
+ # 像素定位操作
470
+ center = Point(640, 360)
471
+ offset = center.offset(10, -5) # 向右移动10像素,向下移动5像素
472
+
473
+ # 点与点的运算
474
+ distance = center.distance_to(button)
475
+ relative_pos = button - center # 相对位置
476
+
477
+ # 检查点是否相等
478
+ if center == Point(640, 360):
479
+ print("找到了中心点")
480
+
481
+ # 与浮点数运算时会自动转换为PointF
482
+ precise = center + (5.5, 3.2) # 结果为PointF类型
483
+ ```
484
+ """
485
+ __slots__ = (*_BasePoint.__slots__,)
133
486
 
134
487
  class Rect:
135
488
  """
136
- 矩形类。
489
+ ## Rect
490
+ 表示一个矩形区域,支持多种坐标格式和几何操作。
491
+
492
+ ### 例
493
+ ```python
494
+ # 创建矩形
495
+ rect = Rect(10, 20, 100, 50, name="按钮区域")
496
+ rect2 = Rect(xywh=(10, 20, 100, 50))
497
+
498
+ # 从两点创建矩形
499
+ rect3 = Rect.from_xyxy(10, 20, 110, 70)
500
+
501
+ # 获取矩形属性
502
+ print(rect.center) # 中心点
503
+ print(rect.size) # (100, 50)
504
+ print(rect.top_left) # 左上角点,其他三个角落同理
505
+
506
+ # 矩形操作
507
+ moved = rect.move(10, 10) # 原地移动
508
+ copied = rect.moved(5, 15) # 移动后的新矩形
509
+ enlarged = rect.inflate(5, 5) # 原地扩大
510
+
511
+ # 几何计算
512
+ if rect.contains_point(Point(50, 40)):
513
+ print("点在矩形内")
514
+
515
+ if rect.intersects_with(other_rect):
516
+ print("两个矩形相交")
517
+
518
+ union = rect.union_of(other_rect) # 并集
519
+ intersection = rect.intersection_of(other_rect) # 交集
520
+ ```
137
521
  """
522
+ __slots__ = ('x1', 'y1', 'w', 'h', 'name')
523
+
138
524
  def __init__(
139
525
  self,
140
526
  x: int | None = None,
@@ -170,13 +556,13 @@ class Rect:
170
556
  else:
171
557
  raise ValueError('Either xywh or x, y, w, h must be provided.')
172
558
 
173
- self.x1 = x
559
+ self.x1 = int(x)
174
560
  """矩形左上角的 X 坐标。"""
175
- self.y1 = y
561
+ self.y1 = int(y)
176
562
  """矩形左上角的 Y 坐标。"""
177
- self.w = w
563
+ self.w = int(w)
178
564
  """矩形的宽度。"""
179
- self.h = h
565
+ self.h = int(h)
180
566
  """矩形的高度。"""
181
567
  self.name: str | None = name
182
568
  """矩形的名称。"""
@@ -187,7 +573,7 @@ class Rect:
187
573
  从 (x1, y1, x2, y2) 创建矩形。
188
574
  :return: 创建结果。
189
575
  """
190
- return cls(x1, y1, x2 - x1, y2 - y1)
576
+ return cls(int(x1), int(y1), int(x2 - x1), int(y2 - y1))
191
577
 
192
578
  @property
193
579
  def x2(self) -> int:
@@ -269,12 +655,86 @@ class Rect:
269
655
  def center(self) -> Point:
270
656
  """
271
657
  矩形的中心点。
658
+
659
+ :return: 中心点 Point 对象。
272
660
  """
273
661
  if self.name:
274
662
  name = "Center of rect "+ self.name
275
663
  else:
276
664
  name = None
277
- return Point(self.x1 + self.w // 2, self.y1 + self.h // 2, name=name)
665
+ return Point(int(self.x1 + self.w / 2), int(self.y1 + self.h / 2), name=name)
666
+
667
+ @property
668
+ def center_x(self) -> int:
669
+ """
670
+ 中心点的 x 坐标。
671
+
672
+ :return: 中心点的 x 坐标。
673
+ """
674
+ return self.x1 + self.w // 2
675
+
676
+ @property
677
+ def center_y(self) -> int:
678
+ """
679
+ 中心点的 y 坐标。
680
+
681
+ :return: 中心点的 y 坐标。
682
+ """
683
+ return self.y1 + self.h // 2
684
+
685
+ @property
686
+ def middle_top(self) -> Point:
687
+ """
688
+ 矩形顶部边的中点。
689
+
690
+ :return: 顶部边的中点。
691
+ """
692
+ return Point(self.center_x, self.y1)
693
+
694
+ @property
695
+ def middle_bottom(self) -> Point:
696
+ """
697
+ 矩形底部边的中点。
698
+
699
+ :return: 底部边的中点。
700
+ """
701
+ return Point(self.center_x, self.y2)
702
+
703
+ @property
704
+ def middle_left(self) -> Point:
705
+ """
706
+ 矩形左侧边的中点。
707
+
708
+ :return: 左侧边的中点。
709
+ """
710
+ return Point(self.x1, self.center_y)
711
+
712
+ @property
713
+ def middle_right(self) -> Point:
714
+ """
715
+ 矩形右侧边的中点。
716
+
717
+ :return: 右侧边的中点。
718
+ """
719
+ return Point(self.x2, self.center_y)
720
+
721
+ @property
722
+ def size(self) -> tuple[int, int]:
723
+ """
724
+ 一个 `(width, height)` 元组。
725
+
726
+ :return: 包含宽度和高度的元组。
727
+ """
728
+ return self.w, self.h
729
+
730
+ @size.setter
731
+ def size(self, value: tuple[int, int]):
732
+ """
733
+ 设置矩形的尺寸。
734
+
735
+ :param value: 包含新宽度和新高度的元组。
736
+ """
737
+ self.w, self.h = value
278
738
 
279
739
  def __repr__(self) -> str:
280
740
  return f'Rect<"{self.name}" at (x={self.x1}, y={self.y1}, w={self.w}, h={self.h})>'
@@ -282,9 +742,326 @@ class Rect:
282
742
  def __str__(self) -> str:
283
743
  return f'(x={self.x1}, y={self.y1}, w={self.w}, h={self.h})'
284
744
 
745
+ def copy(self) -> 'Rect':
746
+ """
747
+ 返回一个与当前对象完全相同的**新** `Rect` 对象。
748
+
749
+ :return: 当前 Rect 对象的一个副本。
750
+ """
751
+ return Rect(self.x1, self.y1, self.w, self.h, name=self.name)
752
+
753
+ def move(self, dx: int, dy: int) -> 'Rect':
754
+ """
755
+ **原地**移动矩形。
756
+
757
+ :param dx: x 方向的移动距离。
758
+ :param dy: y 方向的移动距离。
759
+ :return: 移动后的矩形本身。
760
+ """
761
+ self.x1 += dx
762
+ self.y1 += dy
763
+ return self
764
+
765
+ def moved(self, dx: int, dy: int) -> 'Rect':
766
+ """
767
+ 返回一个移动后的**新** `Rect` 对象。
768
+
769
+ :param dx: x 方向的移动距离。
770
+ :param dy: y 方向的移动距离。
771
+ :return: 移动后的新 Rect 对象。
772
+ """
773
+ return Rect(self.x1 + dx, self.y1 + dy, self.w, self.h, name=self.name)
774
+
775
+ def inflate(self, dx: int, dy: int) -> 'Rect':
776
+ """
777
+ **原地**缩放矩形(中心点不变)。
778
+
779
+ 矩形的宽度增加 `2 * dx`,高度增加 `2 * dy`。
780
+ 负值会缩小矩形。
781
+
782
+ :param dx: 宽度方向的膨胀量(每边)。
783
+ :param dy: 高度方向的膨胀量(每边)。
784
+ :return: 缩放后的矩形本身。
785
+ """
786
+ self.x1 -= dx
787
+ self.y1 -= dy
788
+ self.w += 2 * dx
789
+ self.h += 2 * dy
790
+ return self
791
+
792
+ def inflated(self, dx: int, dy: int) -> 'Rect':
793
+ """
794
+ 返回一个缩放后的**新** `Rect` 对象。
795
+
796
+ :param dx: 宽度方向的膨胀量(每边)。
797
+ param dy: 高度方向的膨胀量(每边)。
798
+ :return: 缩放后的新 Rect 对象。
799
+ """
800
+ return Rect(self.x1 - dx, self.y1 - dy, self.w + 2 * dx, self.h + 2 * dy, name=self.name)
801
+
802
+ def normalize(self) -> 'Rect':
803
+ """
804
+ **原地**修正矩形,确保 `width` 和 `height` 为正数。
805
+
806
+ 如果宽度或高度为负,则交换坐标以使其为正。
807
+
808
+ :return: 修正后的矩形本身。
809
+ """
810
+ if self.w < 0:
811
+ self.x1 += self.w
812
+ self.w = -self.w
813
+ if self.h < 0:
814
+ self.y1 += self.h
815
+ self.h = -self.h
816
+ return self
817
+
818
+ def normalized(self) -> 'Rect':
819
+ """
820
+ 返回一个修正后的**新** `Rect` 对象,确保 `width` 和 `height` 为正数。
821
+
822
+ :return: 修正后的新 Rect 对象。
823
+ """
824
+ x, y, w, h = self.x1, self.y1, self.w, self.h
825
+ if w < 0:
826
+ x += w
827
+ w = -w
828
+ if h < 0:
829
+ y += h
830
+ h = -h
831
+ return Rect(x, y, w, h, name=self.name)
832
+
833
+ def contains_point(self, point: 'AnyPoint | PointTuple | PointFTuple') -> bool:
834
+ """
835
+ 检查一个点是否在此矩形内部。
836
+
837
+ .. note::
838
+ 对于边界值,左边界与上边界包含,而右边界与下边界不包含。
839
+
840
+ 例如 `Rect(0, 0, 10, 10)` 包含 `Point(0, 0)`,但不包含 `Point(10, 10)`。
841
+
842
+ :param point: 要检查的点。
843
+ :return: 如果点在矩形内,则返回 `True`。
844
+ """
845
+ return self.x1 <= point[0] < self.x2 and self.y1 <= point[1] < self.y2
846
+
847
+ def contains_rect(self, other_rect: 'Rect') -> bool:
848
+ """检查此矩形是否完全包含另一个矩形。
849
+
850
+ :param other_rect: 要检查的另一个矩形。
851
+ :return: 是否完全包含。
852
+ """
853
+ # 使用半开区间规则:矩形表示为 [x1, x2) x [y1, y2)
854
+ # 因此 other_rect 完全包含于 self 当且仅当
855
+ # other_rect.x1 >= self.x1, other_rect.y1 >= self.y1,
856
+ # other_rect.x2 <= self.x2, other_rect.y2 <= self.y2
857
+ return (self.x1 <= other_rect.x1 and self.y1 <= other_rect.y1 and
858
+ other_rect.x2 <= self.x2 and other_rect.y2 <= self.y2)
859
+
860
+
861
+ def intersects_with(self, other_rect: 'Rect') -> bool:
862
+ """
863
+ 检查此矩形是否与另一个矩形相交。
864
+
865
+ .. note::
866
+ 若两个矩形只有边界重叠,不算做相交。
867
+
868
+ :param other_rect: 要检查的另一个矩形。
869
+ :return: 如果两个矩形相交,则返回 `True`。
870
+ """
871
+ return not (self.x2 <= other_rect.x1 or self.x1 >= other_rect.x2 or
872
+ self.y2 <= other_rect.y1 or self.y1 >= other_rect.y2)
873
+
874
+ def union_of(self, other_rect: 'Rect') -> 'Rect':
875
+ """
876
+ 返回一个能同时包含两个矩形的**新** `Rect` 对象(并集)。
877
+
878
+ :param other_rect: 要合并的另一个矩形。
879
+ :return: 包含两个矩形的最小矩形。
880
+ """
881
+ x1 = min(self.x1, other_rect.x1)
882
+ y1 = min(self.y1, other_rect.y1)
883
+ x2 = max(self.x2, other_rect.x2)
884
+ y2 = max(self.y2, other_rect.y2)
885
+ return Rect.from_xyxy(x1, y1, x2, y2)
886
+
887
+ def intersection_of(self, other_rect: 'Rect') -> 'Rect | None':
888
+ """
889
+ 返回两个矩形相交区域的**新** `Rect` 对象(交集)。
890
+
891
+ 如果不相交,则返回 `None`。
892
+
893
+ :param other_rect: 要计算交集的另一个矩形。
894
+ :return: 相交区域的矩形,或 `None`。
895
+ """
896
+ x1 = max(self.x1, other_rect.x1)
897
+ y1 = max(self.y1, other_rect.y1)
898
+ x2 = min(self.x2, other_rect.x2)
899
+ y2 = min(self.y2, other_rect.y2)
900
+ if x1 >= x2 or y1 >= y2:
901
+ return None
902
+ return Rect.from_xyxy(x1, y1, x2, y2)
903
+
904
+ def is_empty(self) -> bool:
905
+ """
906
+ 如果矩形的 `width` 或 `height` 小于等于零,则返回 `True`。
907
+
908
+ :return: 如果矩形为空,则返回 `True`。
909
+ """
910
+ return self.w <= 0 or self.h <= 0
911
+
912
+ @overload
913
+ def __contains__(self, obj: 'Point | PointTuple | PointF | PointFTuple') -> bool: ...
914
+ @overload
915
+ def __contains__(self, obj: 'Rect') -> bool: ...
916
+
917
+ def __contains__(self, obj: 'AnyPoint | PointTuple | PointFTuple | Rect') -> bool:
918
+ """
919
+ 判断点或矩形是否被此矩形包含。
920
+
921
+ - 如果传入的是点或点元组,等价于 `Rect.contains_point`。
922
+ - 如果传入的是 `Rect`,则判断整个矩形是否被包含(完全包含)。
923
+
924
+ :param obj: 要检查的点或矩形。
925
+ :return: 如果被包含则返回 `True`。
926
+ """
927
+ # 如果是矩形,则检查矩形包含关系
928
+ if is_rect(obj):
929
+ return self.contains_rect(obj)
930
+ # 如果是任意点类型(Point / PointF)或长度为2的元组,则视为点
931
+ if is_any_point(obj) or (isinstance(obj, tuple) and len(obj) == 2):
932
+ return self.contains_point(obj)
933
+ raise TypeError("Argument must be a Point, PointF, 2-tuple of int or float, or Rect.")
285
934
 
935
+ AnyPoint = Union[Point, PointF]
936
+ """任意 Point 对象,包括 Point 与 PointF。"""
937
+ AnyPointTuple = Union[PointTuple, PointFTuple]
938
+ """任意 Point 元组,包括 tuple[int, int] 与 tuple[float, float]。"""
939
+ PointLike = Union[Point, PointTuple]
940
+ PointFLike = Union[PointF, PointFTuple]
941
+ AnyPointLike = Union[AnyPoint, AnyPointTuple]
942
+ """任意类似于 Point 的对象,包括 Point、PointF、tuple[int, int]、tuple[float, float]。"""
943
+ RectLike = Union[Rect, RectTuple]
944
+ """任意类似于 Rect 的对象,包括 Rect、tuple[int, int, int, int]。"""
286
945
  def is_point(obj: object) -> TypeGuard[Point]:
287
946
  return isinstance(obj, Point)
288
947
 
948
+ def is_point_f(obj: object) -> TypeGuard[PointF]:
949
+ return isinstance(obj, PointF)
950
+
951
+ def is_any_point(obj: object) -> TypeGuard[AnyPoint]:
952
+ return isinstance(obj, (Point, PointF))
953
+
289
954
  def is_rect(obj: object) -> TypeGuard[Rect]:
290
955
  return isinstance(obj, Rect)
956
+
957
+ def unify_point(point: PointLike) -> Point:
958
+ """
959
+ 将点或元组统一转换为 `Point` 对象。
960
+
961
+ :param point: 要转换的点或元组。
962
+ :return: 转换后的 `Point` 对象。
963
+ :raises TypeError: 如果输入类型不正确则抛出。
964
+
965
+ .. note::
966
+ 若输入数据为 float,会被强制转换为 int。
967
+ """
968
+ # If already an integer Point, return it directly
969
+ if is_point(point):
970
+ return point
971
+
972
+ # If it's a PointF, convert to Point by casting coordinates to int
973
+ if is_point_f(point):
974
+ return Point(int(point.x), int(point.y), name=point.name)
975
+
976
+ # If it's a tuple-like (x, y) sequence, attempt to extract
977
+ if isinstance(point, (tuple, list)) and len(point) == 2:
978
+ x, y = point[0], point[1]
979
+ try:
980
+ return Point(int(x), int(y))
981
+ except Exception:
982
+ raise TypeError('Point tuple must contain numeric values')
983
+
984
+ raise TypeError('Argument must be a Point, PointF, or 2-tuple of numbers')
985
+
986
+ def unify_pointf(point: PointLike) -> PointF:
987
+ """
988
+ 将点或元组统一转换为 `PointF` 对象。
989
+
990
+ :param point: 要转换的点或元组。
991
+ :return: 转换后的 `PointF` 对象。
992
+ :raises TypeError: 如果输入类型不正确则抛出。
993
+ """
994
+ # If already a PointF, return it
995
+ if is_point_f(point):
996
+ return point
997
+
998
+ # If it's an integer Point, convert to PointF preserving name
999
+ if is_point(point):
1000
+ return PointF(float(point.x), float(point.y), name=point.name)
1001
+
1002
+ # If it's a tuple-like (x, y), convert elements to float
1003
+ if isinstance(point, (tuple, list)) and len(point) == 2:
1004
+ x, y = point[0], point[1]
1005
+ try:
1006
+ return PointF(float(x), float(y))
1007
+ except Exception:
1008
+ raise TypeError('PointF tuple must contain numeric values')
1009
+
1010
+ raise TypeError('Argument must be a Point, PointF, or 2-tuple of numbers')
1011
+
1012
+ def unify_any_point(point: AnyPointLike) -> Point | PointF:
1013
+ """
1014
+ 将点或元组统一转换为 `Point` 或 `PointF` 对象。
1015
+
1016
+ 如果输入已是 `Point` 或 `PointF` 对象,直接返回。
1017
+ 如果输入是元组,根据其数值类型决定返回类型:
1018
+ - 若所有坐标为整数,返回 `Point`
1019
+ - 若有任何坐标为浮点数,返回 `PointF`
1020
+
1021
+ :param point: 要转换的点或元组。
1022
+ :return: 转换后的 `Point` 或 `PointF` 对象。
1023
+ :raises TypeError: 如果输入类型不正确则抛出。
1024
+ """
1025
+ # If already a Point or PointF, return it directly
1026
+ if is_any_point(point):
1027
+ return point
1028
+
1029
+ # If it's a sequence, check types and convert accordingly
1030
+ if isinstance(point, (tuple, list)) and len(point) == 2:
1031
+ x, y = point[0], point[1]
1032
+ try:
1033
+ # If both are integers, return Point
1034
+ if isinstance(x, int) and isinstance(y, int):
1035
+ return Point(x, y)
1036
+ # Otherwise, return PointF
1037
+ else:
1038
+ return PointF(float(x), float(y))
1039
+ except Exception:
1040
+ raise TypeError('Point tuple must contain numeric values')
1041
+
1042
+ raise TypeError('Argument must be a Point, PointF, or 2-tuple of numbers')
1043
+
1044
+ def unify_rect(rect: RectLike) -> Rect:
1045
+ """
1046
+ 将矩形或元组 (x, y, w, h) 统一转换为 `Rect` 对象。
1047
+
1048
+ :param rect: 要转换的矩形或元组。
1049
+ :return: 转换后的 `Rect` 对象。
1050
+ :raises TypeError: 如果输入类型不正确则抛出。
1051
+
1052
+ .. note::
1053
+ 若输入数据为 float,会被强制转换为 int。
1054
+ """
1055
+ # If already a Rect, return it
1056
+ if is_rect(rect):
1057
+ return rect
1058
+
1059
+ # If it's a tuple-like (x, y, w, h), convert to ints and construct Rect
1060
+ if isinstance(rect, (tuple, list)) and len(rect) == 4:
1061
+ x, y, w, h = rect[0], rect[1], rect[2], rect[3]
1062
+ try:
1063
+ return Rect(int(x), int(y), int(w), int(h))
1064
+ except Exception:
1065
+ raise TypeError('Rect tuple must contain numeric values')
1066
+
1067
+ raise TypeError('Argument must be a Rect or 4-tuple of numbers (x, y, w, h)')