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

204 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-21 22:22 +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.""" 

32 

33from math import sqrt, acos 

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

35 

36try: 

37 from pyTooling.Decorators import readonly, export 

38 from pyTooling.Exceptions import ToolingException 

39 from pyTooling.MetaClasses import ExtendedType 

40 from pyTooling.Common import getFullyQualifiedName 

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

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

43 

44 try: 

45 from Decorators import readonly, export 

46 from Exceptions import ToolingException 

47 from MetaClasses import ExtendedType 

48 from Common import getFullyQualifiedName 

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

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

51 raise ex 

52 

53 

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

55 

56 

57@export 

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

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

60 

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

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

63 

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

65 """ 

66 Initializes a 2-dimensional point. 

67 

68 :param x: X-coordinate. 

69 :param y: Y-coordinate. 

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

71 """ 

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

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

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

75 raise ex 

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

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

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

79 raise ex 

80 

81 self.x = x 

82 self.y = y 

83 

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

85 """ 

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

87 

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

89 

90 .. seealso:: 

91 

92 :meth:`+ operator <__add__>` 

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

94 :meth:`- operator <__sub__>` 

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

96 """ 

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

98 

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

100 """ 

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

102 

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

104 """ 

105 return self.x, self.y 

106 

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

108 """ 

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

110 

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

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

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

114 """ 

115 if isinstance(other, Offset2D): 

116 return self.__class__( 

117 self.x + other.xOffset, 

118 self.y + other.yOffset 

119 ) 

120 elif isinstance(other, tuple): 

121 return self.__class__( 

122 self.x + other[0], 

123 self.y + other[1] 

124 ) 

125 else: 

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

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

128 raise ex 

129 

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

131 """ 

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

133 

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

135 :returns: This 2D-point. 

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

137 """ 

138 if isinstance(other, Offset2D): 

139 self.x += other.xOffset 

140 self.y += other.yOffset 

141 elif isinstance(other, tuple): 

142 self.x += other[0] 

143 self.y += other[1] 

144 else: 

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

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

147 raise ex 

148 

149 return self 

150 

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

152 """ 

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

154 

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

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

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

158 """ 

159 if isinstance(other, Point2D): 

160 return Offset2D( 

161 self.x - other.x, 

162 self.y - other.y 

163 ) 

164 else: 

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

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

167 raise ex 

168 

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

170 """ 

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

172 

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

174 :returns: This 2D-point. 

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

176 """ 

177 if isinstance(other, Offset2D): 

178 self.x -= other.xOffset 

179 self.y -= other.yOffset 

180 elif isinstance(other, tuple): 

181 self.x -= other[0] 

182 self.y -= other[1] 

183 else: 

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

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

186 raise ex 

187 

188 return self 

189 

190 def __repr__(self) -> str: 

191 """ 

192 Returns the 2D point's string representation. 

193 

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

195 """ 

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

197 

198 def __str__(self) -> str: 

199 """ 

200 Returns the 2D point's string equivalent. 

201 

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

203 """ 

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

205 

206 

207@export 

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

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

210 

211 def __init__(self) -> None: 

212 """ 

213 Initializes a 2-dimensional origin. 

214 """ 

215 super().__init__(0, 0) 

216 

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

218 """ 

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

220 """ 

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

222 

223 def __repr__(self) -> str: 

224 """ 

225 Returns the 2D origin's string representation. 

226 

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

228 """ 

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

230 

231 

232@export 

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

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

235 

236 xOffset: Coordinate #: The x-direction offset 

237 yOffset: Coordinate #: The y-direction offset 

238 

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

240 """ 

241 Initializes a 2-dimensional offset. 

242 

243 :param xOffset: x-direction offset. 

244 :param yOffset: y-direction offset. 

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

246 """ 

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

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

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

250 raise ex 

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

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

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

254 raise ex 

255 

256 self.xOffset = xOffset 

257 self.yOffset = yOffset 

258 

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

260 """ 

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

262 

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

264 

265 .. seealso:: 

266 

267 :meth:`+ operator <__add__>` 

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

269 :meth:`- operator <__sub__>` 

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

271 """ 

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

273 

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

275 """ 

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

277 

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

279 """ 

280 return self.xOffset, self.yOffset 

281 

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

283 """ 

284 Compare two 2D-offsets for equality. 

285 

286 :param other: Parameter to compare against. 

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

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

289 """ 

290 if isinstance(other, Offset2D): 

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

292 elif isinstance(other, tuple): 

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

294 else: 

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

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

297 raise ex 

298 

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

300 """ 

301 Compare two 2D-offsets for inequality. 

302 

303 :param other: Parameter to compare against. 

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

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

306 """ 

307 return not self.__eq__(other) 

308 

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

310 """ 

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

312 

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

314 """ 

315 return self.__class__( 

316 -self.xOffset, 

317 -self.yOffset 

318 ) 

319 

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

321 """ 

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

323 

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

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

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

327 """ 

328 if isinstance(other, Offset2D): 

329 return self.__class__( 

330 self.xOffset + other.xOffset, 

331 self.yOffset + other.yOffset 

332 ) 

333 elif isinstance(other, tuple): 

334 return self.__class__( 

335 self.xOffset + other[0], 

336 self.yOffset + other[1] 

337 ) 

338 else: 

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

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

341 raise ex 

342 

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

344 """ 

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

346 

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

348 :returns: This 2D-point. 

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

350 """ 

351 if isinstance(other, Offset2D): 

352 self.xOffset += other.xOffset 

353 self.yOffset += other.yOffset 

354 elif isinstance(other, tuple): 

355 self.xOffset += other[0] 

356 self.yOffset += other[1] 

357 else: 

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

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

360 raise ex 

361 

362 return self 

363 

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

365 """ 

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

367 

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

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

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

371 """ 

372 if isinstance(other, Offset2D): 

373 return self.__class__( 

374 self.xOffset - other.xOffset, 

375 self.yOffset - other.yOffset 

376 ) 

377 elif isinstance(other, tuple): 

378 return self.__class__( 

379 self.xOffset - other[0], 

380 self.yOffset - other[1] 

381 ) 

382 else: 

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

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

385 raise ex 

386 

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

388 """ 

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

390 

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

392 :returns: This 2D-point. 

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

394 """ 

395 if isinstance(other, Offset2D): 

396 self.xOffset -= other.xOffset 

397 self.yOffset -= other.yOffset 

398 elif isinstance(other, tuple): 

399 self.xOffset -= other[0] 

400 self.yOffset -= other[1] 

401 else: 

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

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

404 raise ex 

405 

406 return self 

407 

408 def __repr__(self) -> str: 

409 """ 

410 Returns the 2D offset's string representation. 

411 

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

413 """ 

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

415 

416 def __str__(self) -> str: 

417 """ 

418 Returns the 2D offset's string equivalent. 

419 

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

421 """ 

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

423 

424 

425@export 

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

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

428 

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

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

431 

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

433 """ 

434 Initializes a 2-dimensional size. 

435 

436 :param width: width in x-direction. 

437 :param height: height in y-direction. 

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

439 """ 

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

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

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

443 raise ex 

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

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

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

447 raise ex 

448 

449 self.width = width 

450 self.height = height 

451 

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

453 """ 

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

455 

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

457 """ 

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

459 

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

461 """ 

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

463 

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

465 """ 

466 return self.width, self.height 

467 

468 def __repr__(self) -> str: 

469 """ 

470 Returns the 2D size's string representation. 

471 

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

473 """ 

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

475 

476 def __str__(self) -> str: 

477 """ 

478 Returns the 2D size's string equivalent. 

479 

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

481 """ 

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

483 

484 

485@export 

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

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

488 

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

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

491 

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

493 """ 

494 Initializes a 2-dimensional segment. 

495 

496 :param start: Start point of the segment. 

497 :param end: End point of the segment. 

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

499 """ 

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

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

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

503 raise ex 

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

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

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

507 raise ex 

508 

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

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

511 

512 

513@export 

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

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

516 

517 @readonly 

518 def Length(self) -> float: 

519 """ 

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

521 

522 :return: Euclidean distance between start and end point 

523 """ 

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

525 

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

527 vectorA = self.ToOffset() 

528 vectorB = other.ToOffset() 

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

530 

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

532 

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

534 """ 

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

536 

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

538 """ 

539 return self.end - self.start 

540 

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

542 """ 

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

544 

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

546 """ 

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

548 

549 def __repr__(self) -> str: 

550 """ 

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

552 

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

554 """ 

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

556 

557 def __str__(self) -> str: 

558 """ 

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

560 

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

562 """ 

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