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

188 statements  

« prev     ^ index     » next       coverage.py v7.10.3, created at 2025-08-12 20:40 +0000

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

2# _____ _ _ ____ _ _ ____ ____ # 

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

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

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

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

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

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

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

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

14# Copyright 2025-2025 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.""" 

32from sys import version_info 

33 

34from math import sqrt, acos 

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

36 

37try: 

38 from pyTooling.Decorators import readonly, export 

39 from pyTooling.Exceptions import ToolingException 

40 from pyTooling.MetaClasses import ExtendedType 

41 from pyTooling.Common import getFullyQualifiedName 

42except (ImportError, ModuleNotFoundError): # pragma: no cover 

43 print("[pyTooling.Cartesian2D] Could not import from 'pyTooling.*'!") 

44 

45 try: 

46 from Decorators import readonly, export 

47 from Exceptions import ToolingException 

48 from MetaClasses import ExtendedType 

49 from Common import getFullyQualifiedName 

50 except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover 

51 print("[pyTooling.Cartesian2D] Could not import directly!") 

52 raise ex 

53 

54 

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

56 

57 

58@export 

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

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

61 

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

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

64 

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

66 """ 

67 Initializes a 2-dimensional point. 

68 

69 :param x: X-coordinate. 

70 :param y: Y-coordinate. 

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

72 """ 

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

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

75 if version_info >= (3, 11): # pragma: no cover 

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

77 raise ex 

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

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

80 if version_info >= (3, 11): # pragma: no cover 

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

82 raise ex 

83 

84 self.x = x 

85 self.y = y 

86 

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

88 """ 

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

90 

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

92 

93 .. seealso:: 

94 

95 :meth:`+ operator <__add__>` 

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

97 :meth:`- operator <__sub__>` 

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

99 """ 

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

101 

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

103 """ 

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

105 

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

107 """ 

108 return self.x, self.y 

109 

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

111 """ 

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

113 

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

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

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

117 """ 

118 if isinstance(other, Offset2D): 

119 return self.__class__( 

120 self.x + other.xOffset, 

121 self.y + other.yOffset 

122 ) 

123 elif isinstance(other, tuple): 

124 return self.__class__( 

125 self.x + other[0], 

126 self.y + other[1] 

127 ) 

128 else: 

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

130 if version_info >= (3, 11): # pragma: no cover 

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

132 raise ex 

133 

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

135 """ 

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

137 

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

139 :returns: This 2D-point. 

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

141 """ 

142 if isinstance(other, Offset2D): 

143 self.x += other.xOffset 

144 self.y += other.yOffset 

145 elif isinstance(other, tuple): 

146 self.x += other[0] 

147 self.y += other[1] 

148 else: 

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

150 if version_info >= (3, 11): # pragma: no cover 

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

152 raise ex 

153 

154 return self 

155 

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

157 """ 

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

159 

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

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

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

163 """ 

164 if isinstance(other, Point2D): 

165 return Offset2D( 

166 self.x - other.x, 

167 self.y - other.y 

168 ) 

169 else: 

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

171 if version_info >= (3, 11): # pragma: no cover 

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

173 raise ex 

174 

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

176 """ 

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

178 

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

180 :returns: This 2D-point. 

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

182 """ 

183 if isinstance(other, Offset2D): 

184 self.x -= other.xOffset 

185 self.y -= other.yOffset 

186 elif isinstance(other, tuple): 

187 self.x -= other[0] 

188 self.y -= other[1] 

189 else: 

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

191 if version_info >= (3, 11): # pragma: no cover 

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

193 raise ex 

194 

195 return self 

196 

197 def __repr__(self) -> str: 

198 """ 

199 Returns the 2D point's string representation. 

200 

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

202 """ 

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

204 

205 def __str__(self) -> str: 

206 """ 

207 Returns the 2D point's string equivalent. 

208 

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

210 """ 

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

212 

213 

214@export 

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

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

217 

218 def __init__(self) -> None: 

219 """ 

220 Initializes a 2-dimensional origin. 

221 """ 

222 super().__init__(0, 0) 

223 

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

225 """ 

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

227 """ 

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

229 

230 def __repr__(self) -> str: 

231 """ 

232 Returns the 2D origin's string representation. 

233 

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

235 """ 

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

237 

238 

239@export 

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

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

242 

243 xOffset: Coordinate #: The x-direction offset 

244 yOffset: Coordinate #: The y-direction offset 

245 

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

247 """ 

248 Initializes a 2-dimensional offset. 

249 

250 :param xOffset: x-direction offset. 

251 :param yOffset: y-direction offset. 

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

253 """ 

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

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

256 if version_info >= (3, 11): # pragma: no cover 

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

258 raise ex 

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

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

261 if version_info >= (3, 11): # pragma: no cover 

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

263 raise ex 

264 

265 self.xOffset = xOffset 

266 self.yOffset = yOffset 

267 

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

269 """ 

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

271 

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

273 

274 .. seealso:: 

275 

276 :meth:`+ operator <__add__>` 

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

278 :meth:`- operator <__sub__>` 

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

280 """ 

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

282 

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

284 """ 

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

286 

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

288 """ 

289 return self.xOffset, self.yOffset 

290 

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

292 """ 

293 Compare two 2D-offsets for equality. 

294 

295 :param other: Parameter to compare against. 

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

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

298 """ 

299 if isinstance(other, Offset2D): 

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

301 elif isinstance(other, tuple): 

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

303 else: 

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

305 if version_info >= (3, 11): # pragma: no cover 

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

307 raise ex 

308 

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

310 """ 

311 Compare two 2D-offsets for inequality. 

312 

313 :param other: Parameter to compare against. 

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

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

316 """ 

317 return not self.__eq__(other) 

318 

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

320 """ 

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

322 

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

324 """ 

325 return self.__class__( 

326 -self.xOffset, 

327 -self.yOffset 

328 ) 

329 

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

331 """ 

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

333 

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

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

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

337 """ 

338 if isinstance(other, Offset2D): 

339 return self.__class__( 

340 self.xOffset + other.xOffset, 

341 self.yOffset + other.yOffset 

342 ) 

343 elif isinstance(other, tuple): 

344 return self.__class__( 

345 self.xOffset + other[0], 

346 self.yOffset + other[1] 

347 ) 

348 else: 

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

350 if version_info >= (3, 11): # pragma: no cover 

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

352 raise ex 

353 

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

355 """ 

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

357 

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

359 :returns: This 2D-point. 

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

361 """ 

362 if isinstance(other, Offset2D): 

363 self.xOffset += other.xOffset 

364 self.yOffset += other.yOffset 

365 elif isinstance(other, tuple): 

366 self.xOffset += other[0] 

367 self.yOffset += other[1] 

368 else: 

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

370 if version_info >= (3, 11): # pragma: no cover 

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

372 raise ex 

373 

374 return self 

375 

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

377 """ 

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

379 

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

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

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

383 """ 

384 if isinstance(other, Offset2D): 

385 return self.__class__( 

386 self.xOffset - other.xOffset, 

387 self.yOffset - other.yOffset 

388 ) 

389 elif isinstance(other, tuple): 

390 return self.__class__( 

391 self.xOffset - other[0], 

392 self.yOffset - other[1] 

393 ) 

394 else: 

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

396 if version_info >= (3, 11): # pragma: no cover 

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

398 raise ex 

399 

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

401 """ 

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

403 

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

405 :returns: This 2D-point. 

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

407 """ 

408 if isinstance(other, Offset2D): 

409 self.xOffset -= other.xOffset 

410 self.yOffset -= other.yOffset 

411 elif isinstance(other, tuple): 

412 self.xOffset -= other[0] 

413 self.yOffset -= other[1] 

414 else: 

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

416 if version_info >= (3, 11): # pragma: no cover 

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

418 raise ex 

419 

420 return self 

421 

422 def __repr__(self) -> str: 

423 """ 

424 Returns the 2D offset's string representation. 

425 

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

427 """ 

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

429 

430 def __str__(self) -> str: 

431 """ 

432 Returns the 2D offset's string equivalent. 

433 

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

435 """ 

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

437 

438 

439@export 

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

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

442 

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

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

445 

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

447 """ 

448 Initializes a 2-dimensional size. 

449 

450 :param width: width in x-direction. 

451 :param height: height in y-direction. 

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

453 """ 

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

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

456 if version_info >= (3, 11): # pragma: no cover 

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

458 raise ex 

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

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

461 if version_info >= (3, 11): # pragma: no cover 

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

463 raise ex 

464 

465 self.width = width 

466 self.height = height 

467 

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

469 """ 

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

471 

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

473 """ 

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

475 

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

477 """ 

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

479 

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

481 """ 

482 return self.width, self.height 

483 

484 def __repr__(self) -> str: 

485 """ 

486 Returns the 2D size's string representation. 

487 

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

489 """ 

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

491 

492 def __str__(self) -> str: 

493 """ 

494 Returns the 2D size's string equivalent. 

495 

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

497 """ 

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

499 

500 

501@export 

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

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

504 

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

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

507 

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

509 """ 

510 Initializes a 2-dimensional segment. 

511 

512 :param start: Start point of the segment. 

513 :param end: End point of the segment. 

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

515 """ 

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

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

518 if version_info >= (3, 11): # pragma: no cover 

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

520 raise ex 

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

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

523 if version_info >= (3, 11): # pragma: no cover 

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

525 raise ex 

526 

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

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

529 

530 

531@export 

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

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

534 

535 @readonly 

536 def Length(self) -> float: 

537 """ 

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

539 

540 :return: Euclidean distance between start and end point 

541 """ 

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

543 

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

545 vectorA = self.ToOffset() 

546 vectorB = other.ToOffset() 

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

548 

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

550 

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

552 """ 

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

554 

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

556 """ 

557 return self.end - self.start 

558 

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

560 """ 

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

562 

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

564 """ 

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

566 

567 def __repr__(self) -> str: 

568 """ 

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

570 

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

572 """ 

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

574 

575 def __str__(self) -> str: 

576 """ 

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

578 

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

580 """ 

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