kotonebot 0.5.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 (103) hide show
  1. kotonebot/__init__.py +39 -39
  2. kotonebot/backend/bot.py +312 -312
  3. kotonebot/backend/color.py +525 -525
  4. kotonebot/backend/context/__init__.py +3 -3
  5. kotonebot/backend/context/context.py +1002 -1002
  6. kotonebot/backend/context/task_action.py +183 -183
  7. kotonebot/backend/core.py +86 -129
  8. kotonebot/backend/debug/entry.py +89 -89
  9. kotonebot/backend/debug/mock.py +78 -78
  10. kotonebot/backend/debug/server.py +222 -222
  11. kotonebot/backend/debug/vars.py +351 -351
  12. kotonebot/backend/dispatch.py +227 -227
  13. kotonebot/backend/flow_controller.py +196 -196
  14. kotonebot/backend/image.py +36 -5
  15. kotonebot/backend/loop.py +222 -208
  16. kotonebot/backend/ocr.py +535 -535
  17. kotonebot/backend/preprocessor.py +103 -103
  18. kotonebot/client/__init__.py +9 -9
  19. kotonebot/client/device.py +369 -529
  20. kotonebot/client/fast_screenshot.py +377 -377
  21. kotonebot/client/host/__init__.py +43 -43
  22. kotonebot/client/host/adb_common.py +101 -107
  23. kotonebot/client/host/custom.py +118 -118
  24. kotonebot/client/host/leidian_host.py +196 -196
  25. kotonebot/client/host/mumu12_host.py +353 -353
  26. kotonebot/client/host/protocol.py +214 -214
  27. kotonebot/client/host/windows_common.py +58 -58
  28. kotonebot/client/implements/__init__.py +65 -70
  29. kotonebot/client/implements/adb.py +89 -89
  30. kotonebot/client/implements/nemu_ipc/__init__.py +11 -11
  31. kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +284 -284
  32. kotonebot/client/implements/nemu_ipc/nemu_ipc.py +327 -327
  33. kotonebot/client/implements/remote_windows.py +188 -188
  34. kotonebot/client/implements/uiautomator2.py +85 -85
  35. kotonebot/client/implements/windows.py +176 -176
  36. kotonebot/client/protocol.py +69 -69
  37. kotonebot/client/registration.py +24 -24
  38. kotonebot/client/scaler.py +467 -0
  39. kotonebot/config/base_config.py +96 -96
  40. kotonebot/config/config.py +61 -0
  41. kotonebot/config/manager.py +36 -36
  42. kotonebot/core/__init__.py +13 -0
  43. kotonebot/core/entities/base.py +182 -0
  44. kotonebot/core/entities/compound.py +75 -0
  45. kotonebot/core/entities/ocr.py +117 -0
  46. kotonebot/core/entities/template_match.py +198 -0
  47. kotonebot/devtools/__init__.py +42 -0
  48. kotonebot/devtools/cli/__init__.py +6 -0
  49. kotonebot/devtools/cli/main.py +53 -0
  50. kotonebot/{tools → devtools}/mirror.py +354 -354
  51. kotonebot/devtools/project/project.py +41 -0
  52. kotonebot/devtools/project/scanner.py +202 -0
  53. kotonebot/devtools/project/schema.py +99 -0
  54. kotonebot/devtools/resgen/__init__.py +42 -0
  55. kotonebot/devtools/resgen/codegen.py +331 -0
  56. kotonebot/devtools/resgen/core.py +94 -0
  57. kotonebot/devtools/resgen/parsers.py +360 -0
  58. kotonebot/devtools/resgen/utils.py +158 -0
  59. kotonebot/devtools/resgen/validation.py +115 -0
  60. kotonebot/devtools/web/dist/assets/bootstrap-icons-BOrJxbIo.woff +0 -0
  61. kotonebot/devtools/web/dist/assets/bootstrap-icons-BtvjY1KL.woff2 +0 -0
  62. kotonebot/devtools/web/dist/assets/ext-language_tools-CD021WJ2.js +2577 -0
  63. kotonebot/devtools/web/dist/assets/index-B_m5f2LF.js +2836 -0
  64. kotonebot/devtools/web/dist/assets/index-BlEDyGGa.css +9 -0
  65. kotonebot/devtools/web/dist/assets/language-client-C9muzqaq.js +128 -0
  66. kotonebot/devtools/web/dist/assets/mode-python-CtHp76XS.js +476 -0
  67. kotonebot/devtools/web/dist/icons/symbol-class.svg +3 -0
  68. kotonebot/devtools/web/dist/icons/symbol-file.svg +3 -0
  69. kotonebot/devtools/web/dist/icons/symbol-method.svg +3 -0
  70. kotonebot/devtools/web/dist/index.html +25 -0
  71. kotonebot/devtools/web/server/__init__.py +0 -0
  72. kotonebot/devtools/web/server/rest_api.py +217 -0
  73. kotonebot/devtools/web/server/server.py +85 -0
  74. kotonebot/errors.py +76 -76
  75. kotonebot/interop/win/__init__.py +11 -9
  76. kotonebot/interop/win/_mouse.py +310 -310
  77. kotonebot/interop/win/message_box.py +313 -313
  78. kotonebot/interop/win/reg.py +37 -37
  79. kotonebot/interop/win/shake_mouse.py +224 -0
  80. kotonebot/interop/win/shortcut.py +43 -43
  81. kotonebot/interop/win/task_dialog.py +513 -513
  82. kotonebot/logging/__init__.py +2 -2
  83. kotonebot/logging/log.py +17 -17
  84. kotonebot/primitives/__init__.py +19 -17
  85. kotonebot/primitives/geometry.py +1067 -862
  86. kotonebot/primitives/visual.py +143 -63
  87. kotonebot/ui/file_host/sensio.py +36 -36
  88. kotonebot/ui/file_host/tmp_send.py +54 -54
  89. kotonebot/ui/pushkit/__init__.py +3 -3
  90. kotonebot/ui/pushkit/image_host.py +88 -88
  91. kotonebot/ui/pushkit/protocol.py +13 -13
  92. kotonebot/ui/pushkit/wxpusher.py +54 -54
  93. kotonebot/ui/user.py +148 -148
  94. kotonebot/util.py +436 -436
  95. {kotonebot-0.5.0.dist-info → kotonebot-0.6.0.dist-info}/METADATA +84 -82
  96. kotonebot-0.6.0.dist-info/RECORD +105 -0
  97. kotonebot-0.6.0.dist-info/entry_points.txt +2 -0
  98. {kotonebot-0.5.0.dist-info → kotonebot-0.6.0.dist-info}/licenses/LICENSE +673 -673
  99. kotonebot/client/implements/adb_raw.py +0 -163
  100. kotonebot-0.5.0.dist-info/RECORD +0 -71
  101. /kotonebot/{tools → devtools/project}/__init__.py +0 -0
  102. {kotonebot-0.5.0.dist-info → kotonebot-0.6.0.dist-info}/WHEEL +0 -0
  103. {kotonebot-0.5.0.dist-info → kotonebot-0.6.0.dist-info}/top_level.txt +0 -0
@@ -1,862 +1,1067 @@
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
51
-
52
- T = TypeVar('T')
53
-
54
- class Vector2D(Generic[T]):
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
- def __init__(self, x: T, y: T, *, name: str | None = None):
78
- self.x = x
79
- self.y = y
80
- self.name: str | None = name
81
- """坐标的名称。"""
82
-
83
- def __getitem__(self, item: int):
84
- if item == 0:
85
- return self.x
86
- elif item == 1:
87
- return self.y
88
- else:
89
- raise IndexError
90
-
91
- def __repr__(self) -> str:
92
- return f'Point<"{self.name}" at ({self.x}, {self.y})>'
93
-
94
- def __str__(self) -> str:
95
- return f'({self.x}, {self.y})'
96
-
97
-
98
- class Vector3D(Generic[T]):
99
- """
100
- ## Vector3D
101
- 表示一个三维向量。
102
-
103
- ### 例
104
- ```python
105
- # 创建三维坐标
106
- v3 = Vector3D(100, 200, 50, name="3D点")
107
- print(v3.x, v3.y, v3.z) # 100, 200, 50
108
-
109
- # 使用索引访问
110
- print(v3[0], v3[1], v3[2]) # 100, 200, 50
111
-
112
- # 解构
113
- x, y, z = v3.xyz # (100, 200, 50)
114
- x, y = v3.xy # (100, 200)
115
-
116
- # 颜色值应用
117
- rgb = Vector3D(255, 128, 64, name="颜色")
118
- print(f"RGB: {rgb.rgb if hasattr(v3, 'rgb') else '未定义'}")
119
- ```
120
- """
121
- def __init__(self, x: T, y: T, z: T, *, name: str | None = None):
122
- self.x = x
123
- self.y = y
124
- self.z = z
125
- self.name: str | None = name
126
- """坐标的名称。"""
127
-
128
- def __getitem__(self, item: int):
129
- if item == 0:
130
- return self.x
131
- elif item == 1:
132
- return self.y
133
- elif item == 2:
134
- return self.z
135
- else:
136
- raise IndexError
137
-
138
- @property
139
- def xyz(self) -> tuple[T, T, T]:
140
- """
141
- 三元组 (x, y, z)。OpenCV 格式的坐标。
142
- """
143
- return self.x, self.y, self.z
144
-
145
- @property
146
- def xy(self) -> tuple[T, T]:
147
- """
148
- 二元组 (x, y)。OpenCV 格式的坐标。
149
- """
150
- return self.x, self.y
151
-
152
- class Vector4D(Generic[T]):
153
- """
154
- ## Vector4D
155
- 此类用于表示四维坐标或向量,通常用于颜色空间(如RGBA)等场景。
156
-
157
- ###
158
- ```python
159
- # 创建四维坐标
160
- v4 = Vector4D(100, 200, 150, 255, name="颜色值")
161
- print(f"RGBA: {v4.x}, {v4.y}, {v4.z}, {v4.w}") # 100, 200, 150, 255
162
-
163
- # 使用索引访问
164
- print(v4[0], v4[1], v4[2], v4[3]) # 100, 200, 150, 255
165
-
166
- # 在颜色空间中使用
167
- color = Vector4D(255, 0, 0, 128, name="半透明红色")
168
- print(f"颜色: {color}")
169
-
170
- # 四维向量运算
171
- v4_2 = Vector4D(50, 50, 50, 100)
172
- # 注意:Vector4D 未定义运算,但可用于数据存储
173
- ```
174
- """
175
- def __init__(self, x: T, y: T, z: T, w: T, *, name: str | None = None):
176
- self.x = x
177
- self.y = y
178
- self.z = z
179
- self.w = w
180
- self.name: str | None = name
181
- """坐标的名称。"""
182
-
183
- def __getitem__(self, item: int):
184
- if item == 0:
185
- return self.x
186
- elif item == 1:
187
- return self.y
188
- elif item == 2:
189
- return self.z
190
- elif item == 3:
191
- return self.w
192
- else:
193
- raise IndexError
194
-
195
- Size = Vector2D[int]
196
- """尺寸。相当于 Vector2D[int]"""
197
- RectTuple = tuple[int, int, int, int]
198
- """矩形。(x, y, w, h)"""
199
- PointTuple = tuple[int, int]
200
- """点。(x, y)"""
201
- PointFTuple = tuple[float, float]
202
- """浮点数点。(x, y)"""
203
-
204
-
205
- Number = TypeVar('Number', int, float)
206
- """数字类型,可以是整数或浮点数。"""
207
- NumberTuple2D = tuple[Number, Number]
208
- class _BasePoint(Vector2D[Number]):
209
- @property
210
- def xy(self) -> NumberTuple2D:
211
- """
212
- 二元组 (x, y)。
213
-
214
- :return: 包含 x 和 y 坐标的元组。
215
- """
216
- return self.x, self.y
217
-
218
- @property
219
- def length(self) -> float:
220
- """
221
- 将点视为从原点出发的向量,其长度(模)。
222
-
223
- :return: 向量长度。
224
- """
225
- return math.sqrt(self.x**2 + self.y**2)
226
-
227
- def distance_to(self, other: 'AnyPoint | AnyPointTuple') -> float:
228
- """
229
- 计算到另一个点的距离。
230
-
231
- :param other: 另一个点或元组。
232
- :return: 距离。
233
- """
234
- return math.sqrt((self.x - other[0])**2 + (self.y - other[1])**2)
235
-
236
- def __eq__(self, value: object) -> bool:
237
- """
238
- 比较两个点是否相等。
239
-
240
- :param value: 另一个点。
241
- :return: 如果坐标相等则返回 True,否则返回 False。
242
- """
243
- if isinstance(value, _BasePoint):
244
- return self.x == value.x and self.y == value.y
245
- return False
246
-
247
- def normalized(self) -> 'PointF':
248
- """
249
- 返回一个新的、方向相同但长度为 1 `PointF` 对象(单位向量)。
250
-
251
- :return: 单位向量。
252
- """
253
- l = self.length
254
- if l == 0:
255
- return PointF(0.0, 0.0, name=self.name)
256
- return PointF(self.x / l, self.y / l, name=self.name)
257
-
258
- def offset(self, dx: int | float, dy: int | float) -> 'AnyPoint':
259
- """
260
- 偏移坐标。
261
-
262
- 如果 self, dx, dy 均为整数,返回 Point。否则返回 PointF。
263
-
264
- :param dx: x方向偏移量。
265
- :param dy: y方向偏移量。
266
- :return: 偏移后的新 Point PointF 对象。
267
- """
268
- new_x = self.x + dx
269
- new_y = self.y + dy
270
- if isinstance(self, PointF) or isinstance(dx, float) or isinstance(dy, float):
271
- return PointF(new_x, new_y, name=self.name)
272
- return Point(int(new_x), int(new_y), name=self.name)
273
-
274
- @overload
275
- def __add__(self: 'Point', other: 'Point | PointTuple') -> 'Point': ...
276
- @overload
277
- def __add__(self, other: 'PointF | PointFTuple') -> 'PointF': ...
278
- @overload
279
- def __add__(self: 'PointF', other: 'AnyPoint | AnyPointTuple') -> 'PointF': ...
280
- def __add__(self, other: 'AnyPoint | AnyPointTuple') -> 'AnyPoint':
281
- """
282
- 与另一个点或元组相加。
283
-
284
- 如果任一操作数为浮点数,则结果提升为 PointF。
285
-
286
- :param other: 另一个 Point/PointF 对象或元组。
287
- :return: 相加后的新 Point 或 PointF 对象。
288
- """
289
- new_x = self.x + other[0]
290
- new_y = self.y + other[1]
291
- # hasattr check for tuple, which does not have .x attribute
292
- if isinstance(self, PointF) or isinstance(other, PointF) or \
293
- (not hasattr(other, 'x') and (isinstance(other[0], float) or isinstance(other[1], float))):
294
- return PointF(new_x, new_y, name=self.name)
295
- return Point(int(new_x), int(new_y), name=self.name)
296
-
297
- @overload
298
- def __sub__(self: 'Point', other: 'Point | PointTuple') -> 'Point': ...
299
- @overload
300
- def __sub__(self, other: 'PointF | PointFTuple') -> 'PointF': ...
301
- @overload
302
- def __sub__(self: 'PointF', other: 'AnyPoint | AnyPointTuple') -> 'PointF': ...
303
- def __sub__(self, other: 'AnyPoint | AnyPointTuple') -> 'AnyPoint':
304
- """
305
- 与另一个点或元组相减。
306
-
307
- 如果任一操作数为浮点数,则结果提升为 PointF。
308
-
309
- :param other: 另一个 Point/PointF 对象或元组。
310
- :return: 相减后的新 Point 或 PointF 对象。
311
- """
312
- new_x = self.x - other[0]
313
- new_y = self.y - other[1]
314
- if isinstance(self, PointF) or isinstance(other, PointF) or \
315
- (not hasattr(other, 'x') and (isinstance(other[0], float) or isinstance(other[1], float))):
316
- return PointF(new_x, new_y, name=self.name)
317
- return Point(int(new_x), int(new_y), name=self.name)
318
-
319
- @overload
320
- def __mul__(self: 'Point', scalar: int) -> 'Point': ...
321
- @overload
322
- def __mul__(self, scalar: float) -> 'PointF': ...
323
- def __mul__(self, scalar: int | float) -> 'AnyPoint':
324
- """
325
- 与标量相乘(缩放)。
326
-
327
- :param scalar: 用于缩放的标量。
328
- :return: 缩放后的新 Point 或 PointF 对象。
329
- """
330
- new_x = self.x * scalar
331
- new_y = self.y * scalar
332
- if isinstance(self, PointF) or isinstance(scalar, float):
333
- return PointF(new_x, new_y, name=self.name)
334
- return Point(int(new_x), int(new_y), name=self.name)
335
-
336
- def __truediv__(self, scalar: int | float) -> 'PointF':
337
- """
338
- 与标量相除(缩放)。总是返回一个 PointF 对象。
339
-
340
- :param scalar: 用于缩放的标量。
341
- :return: 缩放后的新 PointF 对象。
342
- """
343
- if scalar == 0:
344
- raise ValueError("Cannot divide by zero")
345
- return PointF(self.x / scalar, self.y / scalar, name=self.name)
346
-
347
- class PointF(_BasePoint[float]):
348
- """
349
- ## PointF
350
- 表示浮点数坐标点。
351
-
352
- ###
353
- ```python
354
- # 创建浮点数坐标点
355
- p1 = PointF(10.5, 20.7, name="精确位置")
356
- p2 = PointF(5.2, 8.9)
357
-
358
- # 距离计算
359
- distance = p1.distance_to(p2) # 计算到另一点的距离
360
- print(f"距离: {distance:.2f}")
361
-
362
- # 向量运算
363
- result = p1 + p2 # 坐标相加
364
- scaled = p1 * 2 # 坐标缩放
365
-
366
- # 单位向量
367
- unit = p1.normalized() # 转换为单位向量
368
-
369
- # 坐标偏移
370
- moved = p1.offset(5.0, 10.0) # 偏移坐标
371
- ```
372
- """
373
-
374
-
375
- class Point(_BasePoint[int]):
376
- """
377
- ## Point
378
- 表示整数坐标点。
379
-
380
- ###
381
- ```python
382
- # 创建整数坐标点
383
- pixel = Point(1920, 1080, name="屏幕分辨率")
384
- button = Point(100, 200, name="按钮位置")
385
-
386
- # 像素定位操作
387
- center = Point(640, 360)
388
- offset = center.offset(10, -5) # 向右移动10像素,向下移动5像素
389
-
390
- # 点与点的运算
391
- distance = center.distance_to(button)
392
- relative_pos = button - center # 相对位置
393
-
394
- # 检查点是否相等
395
- if center == Point(640, 360):
396
- print("找到了中心点")
397
-
398
- # 与浮点数运算时会自动转换为PointF
399
- precise = center + (5.5, 3.2) # 结果为PointF类型
400
- ```
401
- """
402
-
403
- class Rect:
404
- """
405
- ## Rect
406
- 表示一个矩形区域,支持多种坐标格式和几何操作。
407
-
408
- ### 例
409
- ```python
410
- # 创建矩形
411
- rect = Rect(10, 20, 100, 50, name="按钮区域")
412
- rect2 = Rect(xywh=(10, 20, 100, 50))
413
-
414
- # 从两点创建矩形
415
- rect3 = Rect.from_xyxy(10, 20, 110, 70)
416
-
417
- # 获取矩形属性
418
- print(rect.center) # 中心点
419
- print(rect.size) # (100, 50)
420
- print(rect.top_left) # 左上角点,其他三个角落同理
421
-
422
- # 矩形操作
423
- moved = rect.move(10, 10) # 原地移动
424
- copied = rect.moved(5, 15) # 移动后的新矩形
425
- enlarged = rect.inflate(5, 5) # 原地扩大
426
-
427
- # 几何计算
428
- if rect.contains_point(Point(50, 40)):
429
- print("点在矩形内")
430
-
431
- if rect.intersects_with(other_rect):
432
- print("两个矩形相交")
433
-
434
- union = rect.union_of(other_rect) # 并集
435
- intersection = rect.intersection_of(other_rect) # 交集
436
- ```
437
- """
438
- def __init__(
439
- self,
440
- x: int | None = None,
441
- y: int | None = None,
442
- w: int | None = None,
443
- h: int | None = None,
444
- *,
445
- xywh: RectTuple | None = None,
446
- name: str | None = None,
447
- ):
448
- """
449
- 从给定的坐标信息创建矩形。
450
-
451
- 参数 `x`, `y`, `w`, `h` 和 `xywh` 必须至少指定一组。
452
-
453
- :param x: 矩形左上角的 X 坐标。
454
- :param y: 矩形左上角的 Y 坐标。
455
- :param w: 矩形的宽度。
456
- :param h: 矩形的高度。
457
- :param xywh: 四元组 (x, y, w, h)。
458
- :param name: 矩形的名称。
459
- :raises ValueError: 提供的坐标参数不完整时抛出。
460
- """
461
- if xywh is not None:
462
- x, y, w, h = xywh
463
- elif (
464
- x is not None and
465
- y is not None and
466
- w is not None and
467
- h is not None
468
- ):
469
- pass
470
- else:
471
- raise ValueError('Either xywh or x, y, w, h must be provided.')
472
-
473
- self.x1 = x
474
- """矩形左上角的 X 坐标。"""
475
- self.y1 = y
476
- """矩形左上角的 Y 坐标。"""
477
- self.w = w
478
- """矩形的宽度。"""
479
- self.h = h
480
- """矩形的高度。"""
481
- self.name: str | None = name
482
- """矩形的名称。"""
483
-
484
- @classmethod
485
- def from_xyxy(cls, x1: int, y1: int, x2: int, y2: int) -> 'Rect':
486
- """
487
- (x1, y1, x2, y2) 创建矩形。
488
- :return: 创建结果。
489
- """
490
- return cls(x1, y1, x2 - x1, y2 - y1)
491
-
492
- @property
493
- def x2(self) -> int:
494
- """矩形右下角的 X 坐标。"""
495
- return self.x1 + self.w
496
-
497
- @x2.setter
498
- def x2(self, value: int):
499
- self.w = value - self.x1
500
-
501
- @property
502
- def y2(self) -> int:
503
- """矩形右下角的 Y 坐标。"""
504
- return self.y1 + self.h
505
-
506
- @y2.setter
507
- def y2(self, value: int):
508
- self.h = value - self.y1
509
-
510
- @property
511
- def xywh(self) -> RectTuple:
512
- """
513
- 四元组 (x1, y1, w, h)。OpenCV 格式的坐标。
514
- """
515
- return self.x1, self.y1, self.w, self.h
516
-
517
- @property
518
- def xyxy(self) -> RectTuple:
519
- """
520
- 四元组 (x1, y1, x2, y2)。
521
- """
522
- return self.x1, self.y1, self.x2, self.y2
523
-
524
- @property
525
- def top_left(self) -> Point:
526
- """
527
- 矩形的左上角点。
528
- """
529
- if self.name:
530
- name = "Left-top of rect "+ self.name
531
- else:
532
- name = None
533
- return Point(self.x1, self.y1, name=name)
534
-
535
- @property
536
- def bottom_right(self) -> Point:
537
- """
538
- 矩形的右下角点。
539
- """
540
- if self.name:
541
- name = "Right-bottom of rect "+ self.name
542
- else:
543
- name = None
544
- return Point(self.x2, self.y2, name=name)
545
-
546
- @property
547
- def left_bottom(self) -> Point:
548
- """
549
- 矩形的左下角点。
550
- """
551
- if self.name:
552
- name = "Left-bottom of rect "+ self.name
553
- else:
554
- name = None
555
- return Point(self.x1, self.y2, name=name)
556
-
557
- @property
558
- def right_top(self) -> Point:
559
- """
560
- 矩形的右上角点。
561
- """
562
- if self.name:
563
- name = "Right-top of rect "+ self.name
564
- else:
565
- name = None
566
- return Point(self.x2, self.y1, name=name)
567
-
568
- @property
569
- def center(self) -> Point:
570
- """
571
- 矩形的中心点。
572
-
573
- :return: 中心点 Point 对象。
574
- """
575
- if self.name:
576
- name = "Center of rect "+ self.name
577
- else:
578
- name = None
579
- return Point(int(self.x1 + self.w / 2), int(self.y1 + self.h / 2), name=name)
580
-
581
- @property
582
- def center_x(self) -> int:
583
- """
584
- 中心点的 x 坐标。
585
-
586
- :return: 中心点的 x 坐标。
587
- """
588
- return self.x1 + self.w // 2
589
-
590
- @property
591
- def center_y(self) -> int:
592
- """
593
- 中心点的 y 坐标。
594
-
595
- :return: 中心点的 y 坐标。
596
- """
597
- return self.y1 + self.h // 2
598
-
599
- @property
600
- def middle_top(self) -> Point:
601
- """
602
- 矩形顶部边的中点。
603
-
604
- :return: 顶部边的中点。
605
- """
606
- return Point(self.center_x, self.y1)
607
-
608
- @property
609
- def middle_bottom(self) -> Point:
610
- """
611
- 矩形底部边的中点。
612
-
613
- :return: 底部边的中点。
614
- """
615
- return Point(self.center_x, self.y2)
616
-
617
- @property
618
- def middle_left(self) -> Point:
619
- """
620
- 矩形左侧边的中点。
621
-
622
- :return: 左侧边的中点。
623
- """
624
- return Point(self.x1, self.center_y)
625
-
626
- @property
627
- def middle_right(self) -> Point:
628
- """
629
- 矩形右侧边的中点。
630
-
631
- :return: 右侧边的中点。
632
- """
633
- return Point(self.x2, self.center_y)
634
-
635
- @property
636
- def size(self) -> tuple[int, int]:
637
- """
638
- 一个 `(width, height)` 元组。
639
-
640
- :return: 包含宽度和高度的元组。
641
- """
642
- return self.w, self.h
643
-
644
- @size.setter
645
- def size(self, value: tuple[int, int]):
646
- """
647
- 设置矩形的尺寸。
648
-
649
- :param value: 包含新宽度和新高度的元组。
650
- """
651
- self.w, self.h = value
652
-
653
- def __repr__(self) -> str:
654
- return f'Rect<"{self.name}" at (x={self.x1}, y={self.y1}, w={self.w}, h={self.h})>'
655
-
656
- def __str__(self) -> str:
657
- return f'(x={self.x1}, y={self.y1}, w={self.w}, h={self.h})'
658
-
659
- def copy(self) -> 'Rect':
660
- """
661
- 返回一个与当前对象完全相同的**新** `Rect` 对象。
662
-
663
- :return: 当前 Rect 对象的一个副本。
664
- """
665
- return Rect(self.x1, self.y1, self.w, self.h, name=self.name)
666
-
667
- def move(self, dx: int, dy: int) -> 'Rect':
668
- """
669
- **原地**移动矩形。
670
-
671
- :param dx: x 方向的移动距离。
672
- :param dy: y 方向的移动距离。
673
- :return: 移动后的矩形本身。
674
- """
675
- self.x1 += dx
676
- self.y1 += dy
677
- return self
678
-
679
- def moved(self, dx: int, dy: int) -> 'Rect':
680
- """
681
- 返回一个移动后的**新** `Rect` 对象。
682
-
683
- :param dx: x 方向的移动距离。
684
- :param dy: y 方向的移动距离。
685
- :return: 移动后的新 Rect 对象。
686
- """
687
- return Rect(self.x1 + dx, self.y1 + dy, self.w, self.h, name=self.name)
688
-
689
- def inflate(self, dx: int, dy: int) -> 'Rect':
690
- """
691
- **原地**缩放矩形(中心点不变)。
692
-
693
- 矩形的宽度增加 `2 * dx`,高度增加 `2 * dy`。
694
- 负值会缩小矩形。
695
-
696
- :param dx: 宽度方向的膨胀量(每边)。
697
- :param dy: 高度方向的膨胀量(每边)。
698
- :return: 缩放后的矩形本身。
699
- """
700
- self.x1 -= dx
701
- self.y1 -= dy
702
- self.w += 2 * dx
703
- self.h += 2 * dy
704
- return self
705
-
706
- def inflated(self, dx: int, dy: int) -> 'Rect':
707
- """
708
- 返回一个缩放后的**新** `Rect` 对象。
709
-
710
- :param dx: 宽度方向的膨胀量(每边)。
711
- param dy: 高度方向的膨胀量(每边)。
712
- :return: 缩放后的新 Rect 对象。
713
- """
714
- return Rect(self.x1 - dx, self.y1 - dy, self.w + 2 * dx, self.h + 2 * dy, name=self.name)
715
-
716
- def normalize(self) -> 'Rect':
717
- """
718
- **原地**修正矩形,确保 `width` 和 `height` 为正数。
719
-
720
- 如果宽度或高度为负,则交换坐标以使其为正。
721
-
722
- :return: 修正后的矩形本身。
723
- """
724
- if self.w < 0:
725
- self.x1 += self.w
726
- self.w = -self.w
727
- if self.h < 0:
728
- self.y1 += self.h
729
- self.h = -self.h
730
- return self
731
-
732
- def normalized(self) -> 'Rect':
733
- """
734
- 返回一个修正后的**新** `Rect` 对象,确保 `width` 和 `height` 为正数。
735
-
736
- :return: 修正后的新 Rect 对象。
737
- """
738
- x, y, w, h = self.x1, self.y1, self.w, self.h
739
- if w < 0:
740
- x += w
741
- w = -w
742
- if h < 0:
743
- y += h
744
- h = -h
745
- return Rect(x, y, w, h, name=self.name)
746
-
747
- def contains_point(self, point: 'AnyPoint | PointTuple | PointFTuple') -> bool:
748
- """
749
- 检查一个点是否在此矩形内部。
750
-
751
- .. note::
752
- 对于边界值,左边界与上边界包含,而右边界与下边界不包含。
753
-
754
- 例如 `Rect(0, 0, 10, 10)` 包含 `Point(0, 0)`,但不包含 `Point(10, 10)`。
755
-
756
- :param point: 要检查的点。
757
- :return: 如果点在矩形内,则返回 `True`。
758
- """
759
- return self.x1 <= point[0] < self.x2 and self.y1 <= point[1] < self.y2
760
-
761
- def contains_rect(self, other_rect: 'Rect') -> bool:
762
- """检查此矩形是否完全包含另一个矩形。
763
-
764
- :param other_rect: 要检查的另一个矩形。
765
- :return: 是否完全包含。
766
- """
767
- # 使用半开区间规则:矩形表示为 [x1, x2) x [y1, y2)
768
- # 因此 other_rect 完全包含于 self 当且仅当
769
- # other_rect.x1 >= self.x1, other_rect.y1 >= self.y1,
770
- # other_rect.x2 <= self.x2, other_rect.y2 <= self.y2
771
- return (self.x1 <= other_rect.x1 and self.y1 <= other_rect.y1 and
772
- other_rect.x2 <= self.x2 and other_rect.y2 <= self.y2)
773
-
774
-
775
- def intersects_with(self, other_rect: 'Rect') -> bool:
776
- """
777
- 检查此矩形是否与另一个矩形相交。
778
-
779
- .. note::
780
- 若两个矩形只有边界重叠,不算做相交。
781
-
782
- :param other_rect: 要检查的另一个矩形。
783
- :return: 如果两个矩形相交,则返回 `True`。
784
- """
785
- return not (self.x2 <= other_rect.x1 or self.x1 >= other_rect.x2 or
786
- self.y2 <= other_rect.y1 or self.y1 >= other_rect.y2)
787
-
788
- def union_of(self, other_rect: 'Rect') -> 'Rect':
789
- """
790
- 返回一个能同时包含两个矩形的**新** `Rect` 对象(并集)。
791
-
792
- :param other_rect: 要合并的另一个矩形。
793
- :return: 包含两个矩形的最小矩形。
794
- """
795
- x1 = min(self.x1, other_rect.x1)
796
- y1 = min(self.y1, other_rect.y1)
797
- x2 = max(self.x2, other_rect.x2)
798
- y2 = max(self.y2, other_rect.y2)
799
- return Rect.from_xyxy(x1, y1, x2, y2)
800
-
801
- def intersection_of(self, other_rect: 'Rect') -> 'Rect | None':
802
- """
803
- 返回两个矩形相交区域的**新** `Rect` 对象(交集)。
804
-
805
- 如果不相交,则返回 `None`。
806
-
807
- :param other_rect: 要计算交集的另一个矩形。
808
- :return: 相交区域的矩形,或 `None`。
809
- """
810
- x1 = max(self.x1, other_rect.x1)
811
- y1 = max(self.y1, other_rect.y1)
812
- x2 = min(self.x2, other_rect.x2)
813
- y2 = min(self.y2, other_rect.y2)
814
- if x1 >= x2 or y1 >= y2:
815
- return None
816
- return Rect.from_xyxy(x1, y1, x2, y2)
817
-
818
- def is_empty(self) -> bool:
819
- """
820
- 如果矩形的 `width` `height` 小于等于零,则返回 `True`。
821
-
822
- :return: 如果矩形为空,则返回 `True`。
823
- """
824
- return self.w <= 0 or self.h <= 0
825
-
826
- @overload
827
- def __contains__(self, obj: 'Point | PointTuple | PointF | PointFTuple') -> bool: ...
828
- @overload
829
- def __contains__(self, obj: 'Rect') -> bool: ...
830
-
831
- def __contains__(self, obj: 'AnyPoint | PointTuple | PointFTuple | Rect') -> bool:
832
- """
833
- 判断点或矩形是否被此矩形包含。
834
-
835
- - 如果传入的是点或点元组,等价于 `Rect.contains_point`。
836
- - 如果传入的是 `Rect`,则判断整个矩形是否被包含(完全包含)。
837
-
838
- :param obj: 要检查的点或矩形。
839
- :return: 如果被包含则返回 `True`。
840
- """
841
- # 如果是矩形,则检查矩形包含关系
842
- if is_rect(obj):
843
- return self.contains_rect(obj)
844
- # 如果是任意点类型(Point / PointF)或长度为2的元组,则视为点
845
- if is_any_point(obj) or (isinstance(obj, tuple) and len(obj) == 2):
846
- return self.contains_point(obj)
847
- raise TypeError("Argument must be a Point, PointF, 2-tuple of int or float, or Rect.")
848
-
849
- AnyPoint = Union[Point, PointF]
850
- AnyPointTuple = Union[PointTuple, PointFTuple]
851
-
852
- def is_point(obj: object) -> TypeGuard[Point]:
853
- return isinstance(obj, Point)
854
-
855
- def is_point_f(obj: object) -> TypeGuard[PointF]:
856
- return isinstance(obj, PointF)
857
-
858
- def is_any_point(obj: object) -> TypeGuard[AnyPoint]:
859
- return isinstance(obj, (Point, PointF))
860
-
861
- def is_rect(obj: object) -> TypeGuard[Rect]:
862
- return isinstance(obj, Rect)
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
51
+
52
+ T = TypeVar('T')
53
+
54
+ class Vector2D(Generic[T]):
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
+
79
+ def __init__(self, x: T, y: T, *, name: str | None = None):
80
+ self.x = x
81
+ self.y = y
82
+ self.name: str | None = name
83
+ """坐标的名称。"""
84
+
85
+ def __getitem__(self, item: int):
86
+ if item == 0:
87
+ return self.x
88
+ elif item == 1:
89
+ return self.y
90
+ else:
91
+ raise IndexError
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
+
101
+ def __repr__(self) -> str:
102
+ return f'Point<"{self.name}" at ({self.x}, {self.y})>'
103
+
104
+ def __str__(self) -> str:
105
+ return f'({self.x}, {self.y})'
106
+
107
+
108
+ class Vector3D(Generic[T]):
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
+
133
+ def __init__(self, x: T, y: T, z: T, *, name: str | None = None):
134
+ self.x = x
135
+ self.y = y
136
+ self.z = z
137
+ self.name: str | None = name
138
+ """坐标的名称。"""
139
+
140
+ def __getitem__(self, item: int):
141
+ if item == 0:
142
+ return self.x
143
+ elif item == 1:
144
+ return self.y
145
+ elif item == 2:
146
+ return self.z
147
+ else:
148
+ raise IndexError
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
+
159
+ @property
160
+ def xyz(self) -> tuple[T, T, T]:
161
+ """
162
+ 三元组 (x, y, z)。OpenCV 格式的坐标。
163
+ """
164
+ return self.x, self.y, self.z
165
+
166
+ @property
167
+ def xy(self) -> tuple[T, T]:
168
+ """
169
+ 二元组 (x, y)。OpenCV 格式的坐标。
170
+ """
171
+ return self.x, self.y
172
+
173
+ class Vector4D(Generic[T]):
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
+
198
+ def __init__(self, x: T, y: T, z: T, w: T, *, name: str | None = None):
199
+ self.x = x
200
+ self.y = y
201
+ self.z = z
202
+ self.w = w
203
+ self.name: str | None = name
204
+ """坐标的名称。"""
205
+
206
+ def __getitem__(self, item: int):
207
+ if item == 0:
208
+ return self.x
209
+ elif item == 1:
210
+ return self.y
211
+ elif item == 2:
212
+ return self.z
213
+ elif item == 3:
214
+ return self.w
215
+ else:
216
+ raise IndexError
217
+
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
+
228
+ RectTuple = tuple[int, int, int, int]
229
+ """矩形。(x, y, w, h)"""
230
+ PointTuple = tuple[int, int]
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
+
241
+
242
+ Number = TypeVar('Number', int, float)
243
+ """数字类型,可以是整数或浮点数。"""
244
+ NumberTuple2D = tuple[Number, Number]
245
+ class _BasePoint(Vector2D[Number]):
246
+ __slots__ = (*Vector2D.__slots__,)
247
+
248
+ @property
249
+ def xy(self) -> NumberTuple2D:
250
+ """
251
+ 二元组 (x, y)。
252
+
253
+ :return: 包含 x 和 y 坐标的元组。
254
+ """
255
+ return self.x, self.y
256
+
257
+ @property
258
+ def length(self) -> float:
259
+ """
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
+ 计算到另一个点的距离。
269
+
270
+ :param other: 另一个点或元组。
271
+ :return: 距离。
272
+ """
273
+ return math.sqrt((self.x - other[0])**2 + (self.y - other[1])**2)
274
+
275
+ def __eq__(self, value: object) -> bool:
276
+ """
277
+ 比较两个点是否相等。
278
+
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:
287
+ """
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]
305
+ else:
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。
367
+
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':
386
+ """
387
+ 与另一个点或元组相减。
388
+
389
+ 如果任一操作数为浮点数,则结果提升为 PointF。
390
+
391
+ :param other: 另一个 Point/PointF 对象或元组。
392
+ :return: 相减后的新 Point PointF 对象。
393
+ """
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__,)
486
+
487
+ class Rect:
488
+ """
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
+ ```
521
+ """
522
+ __slots__ = ('x1', 'y1', 'w', 'h', 'name')
523
+
524
+ def __init__(
525
+ self,
526
+ x: int | None = None,
527
+ y: int | None = None,
528
+ w: int | None = None,
529
+ h: int | None = None,
530
+ *,
531
+ xywh: RectTuple | None = None,
532
+ name: str | None = None,
533
+ ):
534
+ """
535
+ 从给定的坐标信息创建矩形。
536
+
537
+ 参数 `x`, `y`, `w`, `h` 和 `xywh` 必须至少指定一组。
538
+
539
+ :param x: 矩形左上角的 X 坐标。
540
+ :param y: 矩形左上角的 Y 坐标。
541
+ :param w: 矩形的宽度。
542
+ :param h: 矩形的高度。
543
+ :param xywh: 四元组 (x, y, w, h)。
544
+ :param name: 矩形的名称。
545
+ :raises ValueError: 提供的坐标参数不完整时抛出。
546
+ """
547
+ if xywh is not None:
548
+ x, y, w, h = xywh
549
+ elif (
550
+ x is not None and
551
+ y is not None and
552
+ w is not None and
553
+ h is not None
554
+ ):
555
+ pass
556
+ else:
557
+ raise ValueError('Either xywh or x, y, w, h must be provided.')
558
+
559
+ self.x1 = int(x)
560
+ """矩形左上角的 X 坐标。"""
561
+ self.y1 = int(y)
562
+ """矩形左上角的 Y 坐标。"""
563
+ self.w = int(w)
564
+ """矩形的宽度。"""
565
+ self.h = int(h)
566
+ """矩形的高度。"""
567
+ self.name: str | None = name
568
+ """矩形的名称。"""
569
+
570
+ @classmethod
571
+ def from_xyxy(cls, x1: int, y1: int, x2: int, y2: int) -> 'Rect':
572
+ """
573
+ (x1, y1, x2, y2) 创建矩形。
574
+ :return: 创建结果。
575
+ """
576
+ return cls(int(x1), int(y1), int(x2 - x1), int(y2 - y1))
577
+
578
+ @property
579
+ def x2(self) -> int:
580
+ """矩形右下角的 X 坐标。"""
581
+ return self.x1 + self.w
582
+
583
+ @x2.setter
584
+ def x2(self, value: int):
585
+ self.w = value - self.x1
586
+
587
+ @property
588
+ def y2(self) -> int:
589
+ """矩形右下角的 Y 坐标。"""
590
+ return self.y1 + self.h
591
+
592
+ @y2.setter
593
+ def y2(self, value: int):
594
+ self.h = value - self.y1
595
+
596
+ @property
597
+ def xywh(self) -> RectTuple:
598
+ """
599
+ 四元组 (x1, y1, w, h)。OpenCV 格式的坐标。
600
+ """
601
+ return self.x1, self.y1, self.w, self.h
602
+
603
+ @property
604
+ def xyxy(self) -> RectTuple:
605
+ """
606
+ 四元组 (x1, y1, x2, y2)
607
+ """
608
+ return self.x1, self.y1, self.x2, self.y2
609
+
610
+ @property
611
+ def top_left(self) -> Point:
612
+ """
613
+ 矩形的左上角点。
614
+ """
615
+ if self.name:
616
+ name = "Left-top of rect "+ self.name
617
+ else:
618
+ name = None
619
+ return Point(self.x1, self.y1, name=name)
620
+
621
+ @property
622
+ def bottom_right(self) -> Point:
623
+ """
624
+ 矩形的右下角点。
625
+ """
626
+ if self.name:
627
+ name = "Right-bottom of rect "+ self.name
628
+ else:
629
+ name = None
630
+ return Point(self.x2, self.y2, name=name)
631
+
632
+ @property
633
+ def left_bottom(self) -> Point:
634
+ """
635
+ 矩形的左下角点。
636
+ """
637
+ if self.name:
638
+ name = "Left-bottom of rect "+ self.name
639
+ else:
640
+ name = None
641
+ return Point(self.x1, self.y2, name=name)
642
+
643
+ @property
644
+ def right_top(self) -> Point:
645
+ """
646
+ 矩形的右上角点。
647
+ """
648
+ if self.name:
649
+ name = "Right-top of rect "+ self.name
650
+ else:
651
+ name = None
652
+ return Point(self.x2, self.y1, name=name)
653
+
654
+ @property
655
+ def center(self) -> Point:
656
+ """
657
+ 矩形的中心点。
658
+
659
+ :return: 中心点 Point 对象。
660
+ """
661
+ if self.name:
662
+ name = "Center of rect "+ self.name
663
+ else:
664
+ name = None
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
738
+
739
+ def __repr__(self) -> str:
740
+ return f'Rect<"{self.name}" at (x={self.x1}, y={self.y1}, w={self.w}, h={self.h})>'
741
+
742
+ def __str__(self) -> str:
743
+ return f'(x={self.x1}, y={self.y1}, w={self.w}, h={self.h})'
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.")
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]。"""
945
+ def is_point(obj: object) -> TypeGuard[Point]:
946
+ return isinstance(obj, Point)
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
+
954
+ def is_rect(obj: object) -> TypeGuard[Rect]:
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)')