Coverage for pyTooling / Cartesian2D / __init__.py: 93%

202 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-02-13 22:36 +0000

1# ==================================================================================================================== # 

2# _____ _ _ ____ _ _ ____ ____ # 

3# _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|__ _ _ __| |_ ___ ___(_) __ _ _ __ |___ \| _ \ # 

4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | / _` | '__| __/ _ \/ __| |/ _` | '_ \ __) | | | | # 

5# | |_) | |_| || | (_) | (_) | | | | | | (_| || |__| (_| | | | || __/\__ \ | (_| | | | |/ __/| |_| | # 

6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____\__,_|_| \__\___||___/_|\__,_|_| |_|_____|____/ # 

7# |_| |___/ |___/ # 

8# ==================================================================================================================== # 

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

13# ==================================================================================================================== # 

14# Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # 

15# # 

16# Licensed under the Apache License, Version 2.0 (the "License"); # 

17# you may not use this file except in compliance with the License. # 

18# You may obtain a copy of the License at # 

19# # 

20# http://www.apache.org/licenses/LICENSE-2.0 # 

21# # 

22# Unless required by applicable law or agreed to in writing, software # 

23# distributed under the License is distributed on an "AS IS" BASIS, # 

24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

25# See the License for the specific language governing permissions and # 

26# limitations under the License. # 

27# # 

28# SPDX-License-Identifier: Apache-2.0 # 

29# ==================================================================================================================== # 

30# 

31"""An implementation of 2D cartesian data structures for Python.""" 

32 

33from math import sqrt, acos 

34from typing import TypeVar, Union, Generic, Any, Tuple 

35 

36from pyTooling.Decorators import readonly, export 

37from pyTooling.MetaClasses import ExtendedType 

38from pyTooling.Common import getFullyQualifiedName 

39 

40 

41Coordinate = TypeVar("Coordinate", bound=Union[int, float]) 

42 

43 

44@export 

45class Point2D(Generic[Coordinate], metaclass=ExtendedType, slots=True): 

46 """An implementation of a 2D cartesian point.""" 

47 

48 x: Coordinate #: The x-direction coordinate. 

49 y: Coordinate #: The y-direction coordinate. 

50 

51 def __init__(self, x: Coordinate, y: Coordinate) -> None: 

52 """ 

53 Initializes a 2-dimensional point. 

54 

55 :param x: X-coordinate. 

56 :param y: Y-coordinate. 

57 :raises TypeError: If x/y-coordinate is not of type integer or float. 

58 """ 

59 if not isinstance(x, (int, float)): 

60 ex = TypeError(f"Parameter 'x' is not of type integer or float.") 

61 ex.add_note(f"Got type '{getFullyQualifiedName(x)}'.") 

62 raise ex 

63 if not isinstance(y, (int, float)): 

64 ex = TypeError(f"Parameter 'y' is not of type integer or float.") 

65 ex.add_note(f"Got type '{getFullyQualifiedName(y)}'.") 

66 raise ex 

67 

68 self.x = x 

69 self.y = y 

70 

71 def Copy(self) -> "Point2D[Coordinate]": # TODO: Python 3.11: -> Self: 

72 """ 

73 Create a new 2D-point as a copy of this 2D point. 

74 

75 :returns: Copy of this 2D-point. 

76 

77 .. seealso:: 

78 

79 :meth:`+ operator <__add__>` 

80 Create a new 2D-point moved by a positive 2D-offset. 

81 :meth:`- operator <__sub__>` 

82 Create a new 2D-point moved by a negative 2D-offset. 

83 """ 

84 return self.__class__(self.x, self.y) 

85 

86 def ToTuple(self) -> Tuple[Coordinate, Coordinate]: 

87 """ 

88 Convert this 2D-Point to a simple 2-element tuple. 

89 

90 :returns: ``(x, y)`` tuple. 

91 """ 

92 return self.x, self.y 

93 

94 def __add__(self, other: Any) -> "Point2D[Coordinate]": 

95 """ 

96 Adds a 2D-offset to this 2D-point and creates a new 2D-point. 

97 

98 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`. 

99 :returns: A new 2D-point shifted by the 2D-offset. 

100 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`. 

101 """ 

102 if isinstance(other, Offset2D): 

103 return self.__class__( 

104 self.x + other.xOffset, 

105 self.y + other.yOffset 

106 ) 

107 elif isinstance(other, tuple): 

108 return self.__class__( 

109 self.x + other[0], 

110 self.y + other[1] 

111 ) 

112 else: 

113 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.") 

114 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

115 raise ex 

116 

117 def __iadd__(self, other: Any) -> "Point2D[Coordinate]": # TODO: Python 3.11: -> Self: 

118 """ 

119 Adds a 2D-offset to this 2D-point (inplace). 

120 

121 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`. 

122 :returns: This 2D-point. 

123 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`. 

124 """ 

125 if isinstance(other, Offset2D): 

126 self.x += other.xOffset 

127 self.y += other.yOffset 

128 elif isinstance(other, tuple): 

129 self.x += other[0] 

130 self.y += other[1] 

131 else: 

132 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.") 

133 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

134 raise ex 

135 

136 return self 

137 

138 def __sub__(self, other: Any) -> Union["Offset2D[Coordinate]", "Point2D[Coordinate]"]: 

139 """ 

140 Subtract two 2D-Points from each other and create a new 2D-offset. 

141 

142 :param other: A 2D-point as :class:`Point2D`. 

143 :returns: A new 2D-offset representing the distance between these two points. 

144 :raises TypeError: If parameter 'other' is not a :class:`Point2D`. 

145 """ 

146 if isinstance(other, Point2D): 

147 return Offset2D( 

148 self.x - other.x, 

149 self.y - other.y 

150 ) 

151 else: 

152 ex = TypeError(f"Parameter 'other' is not of type Point2D.") 

153 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

154 raise ex 

155 

156 def __isub__(self, other: Any) -> "Point2D[Coordinate]": # TODO: Python 3.11: -> Self: 

157 """ 

158 Subtracts a 2D-offset to this 2D-point (inplace). 

159 

160 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`. 

161 :returns: This 2D-point. 

162 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`. 

163 """ 

164 if isinstance(other, Offset2D): 

165 self.x -= other.xOffset 

166 self.y -= other.yOffset 

167 elif isinstance(other, tuple): 

168 self.x -= other[0] 

169 self.y -= other[1] 

170 else: 

171 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.") 

172 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

173 raise ex 

174 

175 return self 

176 

177 def __repr__(self) -> str: 

178 """ 

179 Returns the 2D point's string representation. 

180 

181 :returns: The string representation of the 2D point. 

182 """ 

183 return f"Point2D({self.x}, {self.y})" 

184 

185 def __str__(self) -> str: 

186 """ 

187 Returns the 2D point's string equivalent. 

188 

189 :returns: The string equivalent of the 2D point. 

190 """ 

191 return f"({self.x}, {self.y})" 

192 

193 

194@export 

195class Origin2D(Point2D[Coordinate], Generic[Coordinate]): 

196 """An implementation of a 2D cartesian origin.""" 

197 

198 def __init__(self) -> None: 

199 """ 

200 Initializes a 2-dimensional origin. 

201 """ 

202 super().__init__(0, 0) 

203 

204 def Copy(self) -> "Origin2D[Coordinate]": # TODO: Python 3.11: -> Self: 

205 """ 

206 :raises RuntimeError: Because an origin can't be copied. 

207 """ 

208 raise RuntimeError(f"An origin can't be copied.") 

209 

210 def __repr__(self) -> str: 

211 """ 

212 Returns the 2D origin's string representation. 

213 

214 :returns: The string representation of the 2D origin. 

215 """ 

216 return f"Origin2D({self.x}, {self.y})" 

217 

218 

219@export 

220class Offset2D(Generic[Coordinate], metaclass=ExtendedType, slots=True): 

221 """An implementation of a 2D cartesian offset.""" 

222 

223 xOffset: Coordinate #: The x-direction offset 

224 yOffset: Coordinate #: The y-direction offset 

225 

226 def __init__(self, xOffset: Coordinate, yOffset: Coordinate) -> None: 

227 """ 

228 Initializes a 2-dimensional offset. 

229 

230 :param xOffset: x-direction offset. 

231 :param yOffset: y-direction offset. 

232 :raises TypeError: If x/y-offset is not of type integer or float. 

233 """ 

234 if not isinstance(xOffset, (int, float)): 

235 ex = TypeError(f"Parameter 'xOffset' is not of type integer or float.") 

236 ex.add_note(f"Got type '{getFullyQualifiedName(xOffset)}'.") 

237 raise ex 

238 if not isinstance(yOffset, (int, float)): 

239 ex = TypeError(f"Parameter 'yOffset' is not of type integer or float.") 

240 ex.add_note(f"Got type '{getFullyQualifiedName(yOffset)}'.") 

241 raise ex 

242 

243 self.xOffset = xOffset 

244 self.yOffset = yOffset 

245 

246 def Copy(self) -> "Offset2D[Coordinate]": # TODO: Python 3.11: -> Self: 

247 """ 

248 Create a new 2D-offset as a copy of this 2D-offset. 

249 

250 :returns: Copy of this 2D-offset. 

251 

252 .. seealso:: 

253 

254 :meth:`+ operator <__add__>` 

255 Create a new 2D-offset moved by a positive 2D-offset. 

256 :meth:`- operator <__sub__>` 

257 Create a new 2D-offset moved by a negative 2D-offset. 

258 """ 

259 return self.__class__(self.xOffset, self.yOffset) 

260 

261 def ToTuple(self) -> Tuple[Coordinate, Coordinate]: 

262 """ 

263 Convert this 2D-offset to a simple 2-element tuple. 

264 

265 :returns: ``(x, y)`` tuple. 

266 """ 

267 return self.xOffset, self.yOffset 

268 

269 def __eq__(self, other) -> bool: 

270 """ 

271 Compare two 2D-offsets for equality. 

272 

273 :param other: Parameter to compare against. 

274 :returns: ``True``, if both 2D-offsets are equal. 

275 :raises TypeError: If parameter ``other`` is not of type :class:`Offset2D` or :class:`tuple`. 

276 """ 

277 if isinstance(other, Offset2D): 

278 return self.xOffset == other.xOffset and self.yOffset == other.yOffset 

279 elif isinstance(other, tuple): 

280 return self.xOffset == other[0] and self.yOffset == other[1] 

281 else: 

282 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.") 

283 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

284 raise ex 

285 

286 def __ne__(self, other) -> bool: 

287 """ 

288 Compare two 2D-offsets for inequality. 

289 

290 :param other: Parameter to compare against. 

291 :returns: ``True``, if both 2D-offsets are unequal. 

292 :raises TypeError: If parameter ``other`` is not of type :class:`Offset2D` or :class:`tuple`. 

293 """ 

294 return not self.__eq__(other) 

295 

296 def __neg__(self) -> "Offset2D[Coordinate]": 

297 """ 

298 Negate all components of this 2D-offset and create a new 2D-offset. 

299 

300 :returns: 2D-offset with negated offset components. 

301 """ 

302 return self.__class__( 

303 -self.xOffset, 

304 -self.yOffset 

305 ) 

306 

307 def __add__(self, other: Any) -> "Offset2D[Coordinate]": 

308 """ 

309 Adds a 2D-offset to this 2D-offset and creates a new 2D-offset. 

310 

311 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`. 

312 :returns: A new 2D-offset extended by the 2D-offset. 

313 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`. 

314 """ 

315 if isinstance(other, Offset2D): 

316 return self.__class__( 

317 self.xOffset + other.xOffset, 

318 self.yOffset + other.yOffset 

319 ) 

320 elif isinstance(other, tuple): 

321 return self.__class__( 

322 self.xOffset + other[0], 

323 self.yOffset + other[1] 

324 ) 

325 else: 

326 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.") 

327 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

328 raise ex 

329 

330 def __iadd__(self, other: Any) -> "Offset2D[Coordinate]": # TODO: Python 3.11: -> Self: 

331 """ 

332 Adds a 2D-offset to this 2D-offset (inplace). 

333 

334 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`. 

335 :returns: This 2D-point. 

336 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`. 

337 """ 

338 if isinstance(other, Offset2D): 

339 self.xOffset += other.xOffset 

340 self.yOffset += other.yOffset 

341 elif isinstance(other, tuple): 

342 self.xOffset += other[0] 

343 self.yOffset += other[1] 

344 else: 

345 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.") 

346 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

347 raise ex 

348 

349 return self 

350 

351 def __sub__(self, other: Any) -> "Offset2D[Coordinate]": 

352 """ 

353 Subtracts a 2D-offset from this 2D-offset and creates a new 2D-offset. 

354 

355 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`. 

356 :returns: A new 2D-offset reduced by the 2D-offset. 

357 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`. 

358 """ 

359 if isinstance(other, Offset2D): 

360 return self.__class__( 

361 self.xOffset - other.xOffset, 

362 self.yOffset - other.yOffset 

363 ) 

364 elif isinstance(other, tuple): 

365 return self.__class__( 

366 self.xOffset - other[0], 

367 self.yOffset - other[1] 

368 ) 

369 else: 

370 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.") 

371 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

372 raise ex 

373 

374 def __isub__(self, other: Any) -> "Offset2D[Coordinate]": # TODO: Python 3.11: -> Self: 

375 """ 

376 Subtracts a 2D-offset from this 2D-offset (inplace). 

377 

378 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`. 

379 :returns: This 2D-point. 

380 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`. 

381 """ 

382 if isinstance(other, Offset2D): 

383 self.xOffset -= other.xOffset 

384 self.yOffset -= other.yOffset 

385 elif isinstance(other, tuple): 

386 self.xOffset -= other[0] 

387 self.yOffset -= other[1] 

388 else: 

389 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.") 

390 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

391 raise ex 

392 

393 return self 

394 

395 def __repr__(self) -> str: 

396 """ 

397 Returns the 2D offset's string representation. 

398 

399 :returns: The string representation of the 2D offset. 

400 """ 

401 return f"Offset2D({self.xOffset}, {self.yOffset})" 

402 

403 def __str__(self) -> str: 

404 """ 

405 Returns the 2D offset's string equivalent. 

406 

407 :returns: The string equivalent of the 2D offset. 

408 """ 

409 return f"({self.xOffset}, {self.yOffset})" 

410 

411 

412@export 

413class Size2D(Generic[Coordinate], metaclass=ExtendedType, slots=True): 

414 """An implementation of a 2D cartesian size.""" 

415 

416 width: Coordinate #: width in x-direction. 

417 height: Coordinate #: height in y-direction. 

418 

419 def __init__(self, width: Coordinate, height: Coordinate) -> None: 

420 """ 

421 Initializes a 2-dimensional size. 

422 

423 :param width: width in x-direction. 

424 :param height: height in y-direction. 

425 :raises TypeError: If width/height is not of type integer or float. 

426 """ 

427 if not isinstance(width, (int, float)): 

428 ex = TypeError(f"Parameter 'width' is not of type integer or float.") 

429 ex.add_note(f"Got type '{getFullyQualifiedName(width)}'.") 

430 raise ex 

431 if not isinstance(height, (int, float)): 

432 ex = TypeError(f"Parameter 'height' is not of type integer or float.") 

433 ex.add_note(f"Got type '{getFullyQualifiedName(height)}'.") 

434 raise ex 

435 

436 self.width = width 

437 self.height = height 

438 

439 def Copy(self) -> "Size2D": # TODO: Python 3.11: -> Self: 

440 """ 

441 Create a new 2D-size as a copy of this 2D-size. 

442 

443 :returns: Copy of this 2D-size. 

444 """ 

445 return self.__class__(self.width, self.height) 

446 

447 def ToTuple(self) -> Tuple[Coordinate, Coordinate]: 

448 """ 

449 Convert this 2D-size to a simple 2-element tuple. 

450 

451 :return: ``(width, height)`` tuple. 

452 """ 

453 return self.width, self.height 

454 

455 def __repr__(self) -> str: 

456 """ 

457 Returns the 2D size's string representation. 

458 

459 :returns: The string representation of the 2D size. 

460 """ 

461 return f"Size2D({self.width}, {self.height})" 

462 

463 def __str__(self) -> str: 

464 """ 

465 Returns the 2D size's string equivalent. 

466 

467 :returns: The string equivalent of the 2D size. 

468 """ 

469 return f"({self.width}, {self.height})" 

470 

471 

472@export 

473class Segment2D(Generic[Coordinate], metaclass=ExtendedType, slots=True): 

474 """An implementation of a 2D cartesian segment.""" 

475 

476 start: Point2D[Coordinate] #: Start point of a segment. 

477 end: Point2D[Coordinate] #: End point of a segment. 

478 

479 def __init__(self, start: Point2D[Coordinate], end: Point2D[Coordinate], copyPoints: bool = True) -> None: 

480 """ 

481 Initializes a 2-dimensional segment. 

482 

483 :param start: Start point of the segment. 

484 :param end: End point of the segment. 

485 :raises TypeError: If start/end is not of type Point2D. 

486 """ 

487 if not isinstance(start, Point2D): 487 ↛ 488line 487 didn't jump to line 488 because the condition on line 487 was never true

488 ex = TypeError(f"Parameter 'start' is not of type Point2D.") 

489 ex.add_note(f"Got type '{getFullyQualifiedName(start)}'.") 

490 raise ex 

491 if not isinstance(end, Point2D): 491 ↛ 492line 491 didn't jump to line 492 because the condition on line 491 was never true

492 ex = TypeError(f"Parameter 'end' is not of type Point2D.") 

493 ex.add_note(f"Got type '{getFullyQualifiedName(end)}'.") 

494 raise ex 

495 

496 self.start = start.Copy() if copyPoints else start 

497 self.end = end.Copy() if copyPoints else end 

498 

499 

500@export 

501class LineSegment2D(Segment2D[Coordinate], Generic[Coordinate]): 

502 """An implementation of a 2D cartesian line segment.""" 

503 

504 @readonly 

505 def Length(self) -> float: 

506 """ 

507 Read-only property to return the Euclidean distance between start and end point. 

508 

509 :return: Euclidean distance between start and end point 

510 """ 

511 return sqrt((self.end.x - self.start.x) ** 2 + (self.end.x - self.start.x) ** 2) 

512 

513 def AngleTo(self, other: "LineSegment2D[Coordinate]") -> float: 

514 vectorA = self.ToOffset() 

515 vectorB = other.ToOffset() 

516 scalarProductAB = vectorA.xOffset * vectorB.xOffset + vectorA.yOffset * vectorB.yOffset 

517 

518 return acos(scalarProductAB / (abs(self.Length) * abs(other.Length))) 

519 

520 def ToOffset(self) -> Offset2D[Coordinate]: 

521 """ 

522 Convert this 2D line segment to a 2D-offset. 

523 

524 :return: 2D-offset as :class:`Offset2D` 

525 """ 

526 return self.end - self.start 

527 

528 def ToTuple(self) -> Tuple[Tuple[Coordinate, Coordinate], Tuple[Coordinate, Coordinate]]: 

529 """ 

530 Convert this 2D line segment to a simple 2-element tuple of 2D-point tuples. 

531 

532 :return: ``((x1, y1), (x2, y2))`` tuple. 

533 """ 

534 return self.start.ToTuple(), self.end.ToTuple() 

535 

536 def __repr__(self) -> str: 

537 """ 

538 Returns the 2D line segment's string representation. 

539 

540 :returns: The string representation of the 2D line segment. 

541 """ 

542 return f"LineSegment2D({self.start}, {self.end})" 

543 

544 def __str__(self) -> str: 

545 """ 

546 Returns the 2D line segment's string equivalent. 

547 

548 :returns: The string equivalent of the 2D line segment. 

549 """ 

550 return f"({self.start}{self.end})"