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

205 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-31 22:23 +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 ex.add_note(f"Got type '{getFullyQualifiedName(x)}'.") 

76 raise ex 

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

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

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

80 raise ex 

81 

82 self.x = x 

83 self.y = y 

84 

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

86 """ 

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

88 

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

90 

91 .. seealso:: 

92 

93 :meth:`+ operator <__add__>` 

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

95 :meth:`- operator <__sub__>` 

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

97 """ 

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

99 

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

101 """ 

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

103 

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

105 """ 

106 return self.x, self.y 

107 

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

109 """ 

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

111 

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

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

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

115 """ 

116 if isinstance(other, Offset2D): 

117 return self.__class__( 

118 self.x + other.xOffset, 

119 self.y + other.yOffset 

120 ) 

121 elif isinstance(other, tuple): 

122 return self.__class__( 

123 self.x + other[0], 

124 self.y + other[1] 

125 ) 

126 else: 

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

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

129 raise ex 

130 

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

132 """ 

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

134 

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

136 :returns: This 2D-point. 

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

138 """ 

139 if isinstance(other, Offset2D): 

140 self.x += other.xOffset 

141 self.y += other.yOffset 

142 elif isinstance(other, tuple): 

143 self.x += other[0] 

144 self.y += other[1] 

145 else: 

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

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

148 raise ex 

149 

150 return self 

151 

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

153 """ 

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

155 

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

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

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

159 """ 

160 if isinstance(other, Point2D): 

161 return Offset2D( 

162 self.x - other.x, 

163 self.y - other.y 

164 ) 

165 else: 

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

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

168 raise ex 

169 

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

171 """ 

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

173 

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

175 :returns: This 2D-point. 

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

177 """ 

178 if isinstance(other, Offset2D): 

179 self.x -= other.xOffset 

180 self.y -= other.yOffset 

181 elif isinstance(other, tuple): 

182 self.x -= other[0] 

183 self.y -= other[1] 

184 else: 

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

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

187 raise ex 

188 

189 return self 

190 

191 def __repr__(self) -> str: 

192 """ 

193 Returns the 2D point's string representation. 

194 

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

196 """ 

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

198 

199 def __str__(self) -> str: 

200 """ 

201 Returns the 2D point's string equivalent. 

202 

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

204 """ 

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

206 

207 

208@export 

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

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

211 

212 def __init__(self) -> None: 

213 """ 

214 Initializes a 2-dimensional origin. 

215 """ 

216 super().__init__(0, 0) 

217 

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

219 """ 

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

221 """ 

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

223 

224 def __repr__(self) -> str: 

225 """ 

226 Returns the 2D origin's string representation. 

227 

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

229 """ 

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

231 

232 

233@export 

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

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

236 

237 xOffset: Coordinate #: The x-direction offset 

238 yOffset: Coordinate #: The y-direction offset 

239 

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

241 """ 

242 Initializes a 2-dimensional offset. 

243 

244 :param xOffset: x-direction offset. 

245 :param yOffset: y-direction offset. 

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

247 """ 

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

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

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

251 raise ex 

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

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

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

255 raise ex 

256 

257 self.xOffset = xOffset 

258 self.yOffset = yOffset 

259 

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

261 """ 

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

263 

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

265 

266 .. seealso:: 

267 

268 :meth:`+ operator <__add__>` 

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

270 :meth:`- operator <__sub__>` 

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

272 """ 

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

274 

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

276 """ 

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

278 

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

280 """ 

281 return self.xOffset, self.yOffset 

282 

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

284 """ 

285 Compare two 2D-offsets for equality. 

286 

287 :param other: Parameter to compare against. 

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

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

290 """ 

291 if isinstance(other, Offset2D): 

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

293 elif isinstance(other, tuple): 

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

295 else: 

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

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

298 raise ex 

299 

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

301 """ 

302 Compare two 2D-offsets for inequality. 

303 

304 :param other: Parameter to compare against. 

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

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

307 """ 

308 return not self.__eq__(other) 

309 

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

311 """ 

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

313 

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

315 """ 

316 return self.__class__( 

317 -self.xOffset, 

318 -self.yOffset 

319 ) 

320 

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

322 """ 

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

324 

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

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

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

328 """ 

329 if isinstance(other, Offset2D): 

330 return self.__class__( 

331 self.xOffset + other.xOffset, 

332 self.yOffset + other.yOffset 

333 ) 

334 elif isinstance(other, tuple): 

335 return self.__class__( 

336 self.xOffset + other[0], 

337 self.yOffset + other[1] 

338 ) 

339 else: 

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

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

342 raise ex 

343 

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

345 """ 

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

347 

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

349 :returns: This 2D-point. 

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

351 """ 

352 if isinstance(other, Offset2D): 

353 self.xOffset += other.xOffset 

354 self.yOffset += other.yOffset 

355 elif isinstance(other, tuple): 

356 self.xOffset += other[0] 

357 self.yOffset += other[1] 

358 else: 

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

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

361 raise ex 

362 

363 return self 

364 

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

366 """ 

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

368 

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

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

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

372 """ 

373 if isinstance(other, Offset2D): 

374 return self.__class__( 

375 self.xOffset - other.xOffset, 

376 self.yOffset - other.yOffset 

377 ) 

378 elif isinstance(other, tuple): 

379 return self.__class__( 

380 self.xOffset - other[0], 

381 self.yOffset - other[1] 

382 ) 

383 else: 

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

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

386 raise ex 

387 

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

389 """ 

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

391 

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

393 :returns: This 2D-point. 

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

395 """ 

396 if isinstance(other, Offset2D): 

397 self.xOffset -= other.xOffset 

398 self.yOffset -= other.yOffset 

399 elif isinstance(other, tuple): 

400 self.xOffset -= other[0] 

401 self.yOffset -= other[1] 

402 else: 

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

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

405 raise ex 

406 

407 return self 

408 

409 def __repr__(self) -> str: 

410 """ 

411 Returns the 2D offset's string representation. 

412 

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

414 """ 

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

416 

417 def __str__(self) -> str: 

418 """ 

419 Returns the 2D offset's string equivalent. 

420 

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

422 """ 

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

424 

425 

426@export 

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

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

429 

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

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

432 

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

434 """ 

435 Initializes a 2-dimensional size. 

436 

437 :param width: width in x-direction. 

438 :param height: height in y-direction. 

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

440 """ 

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

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

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

444 raise ex 

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

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

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

448 raise ex 

449 

450 self.width = width 

451 self.height = height 

452 

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

454 """ 

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

456 

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

458 """ 

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

460 

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

462 """ 

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

464 

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

466 """ 

467 return self.width, self.height 

468 

469 def __repr__(self) -> str: 

470 """ 

471 Returns the 2D size's string representation. 

472 

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

474 """ 

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

476 

477 def __str__(self) -> str: 

478 """ 

479 Returns the 2D size's string equivalent. 

480 

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

482 """ 

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

484 

485 

486@export 

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

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

489 

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

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

492 

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

494 """ 

495 Initializes a 2-dimensional segment. 

496 

497 :param start: Start point of the segment. 

498 :param end: End point of the segment. 

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

500 """ 

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

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

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

504 raise ex 

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

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

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

508 raise ex 

509 

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

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

512 

513 

514@export 

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

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

517 

518 @readonly 

519 def Length(self) -> float: 

520 """ 

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

522 

523 :return: Euclidean distance between start and end point 

524 """ 

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

526 

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

528 vectorA = self.ToOffset() 

529 vectorB = other.ToOffset() 

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

531 

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

533 

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

535 """ 

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

537 

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

539 """ 

540 return self.end - self.start 

541 

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

543 """ 

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

545 

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

547 """ 

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

549 

550 def __repr__(self) -> str: 

551 """ 

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

553 

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

555 """ 

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

557 

558 def __str__(self) -> str: 

559 """ 

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

561 

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

563 """ 

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