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

188 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-25 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.""" 

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 return f"Point2D({self.x}, {self.y})" 

199 

200 def __str__(self) -> str: 

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

202 

203 

204@export 

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

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

207 

208 def __init__(self) -> None: 

209 """ 

210 Initializes a 2-dimensional origin. 

211 """ 

212 super().__init__(0, 0) 

213 

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

215 """ 

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

217 """ 

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

219 

220 def __repr__(self) -> str: 

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

222 

223 

224@export 

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

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

227 

228 xOffset: Coordinate #: The x-direction offset 

229 yOffset: Coordinate #: The y-direction offset 

230 

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

232 """ 

233 Initializes a 2-dimensional offset. 

234 

235 :param xOffset: x-direction offset. 

236 :param yOffset: y-direction offset. 

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

238 """ 

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

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

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

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

243 raise ex 

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

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

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

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

248 raise ex 

249 

250 self.xOffset = xOffset 

251 self.yOffset = yOffset 

252 

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

254 """ 

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

256 

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

258 

259 .. seealso:: 

260 

261 :meth:`+ operator <__add__>` 

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

263 :meth:`- operator <__sub__>` 

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

265 """ 

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

267 

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

269 """ 

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

271 

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

273 """ 

274 return self.xOffset, self.yOffset 

275 

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

277 """ 

278 Compare two 2D-offsets for equality. 

279 

280 :param other: Parameter to compare against. 

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

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

283 """ 

284 if isinstance(other, Offset2D): 

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

286 elif isinstance(other, tuple): 

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

288 else: 

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

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

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

292 raise ex 

293 

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

295 """ 

296 Compare two 2D-offsets for inequality. 

297 

298 :param other: Parameter to compare against. 

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

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

301 """ 

302 return not self.__eq__(other) 

303 

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

305 """ 

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

307 

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

309 """ 

310 return self.__class__( 

311 -self.xOffset, 

312 -self.yOffset 

313 ) 

314 

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

316 """ 

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

318 

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

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

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

322 """ 

323 if isinstance(other, Offset2D): 

324 return self.__class__( 

325 self.xOffset + other.xOffset, 

326 self.yOffset + other.yOffset 

327 ) 

328 elif isinstance(other, tuple): 

329 return self.__class__( 

330 self.xOffset + other[0], 

331 self.yOffset + other[1] 

332 ) 

333 else: 

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

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

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

337 raise ex 

338 

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

340 """ 

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

342 

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

344 :returns: This 2D-point. 

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

346 """ 

347 if isinstance(other, Offset2D): 

348 self.xOffset += other.xOffset 

349 self.yOffset += other.yOffset 

350 elif isinstance(other, tuple): 

351 self.xOffset += other[0] 

352 self.yOffset += other[1] 

353 else: 

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

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

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

357 raise ex 

358 

359 return self 

360 

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

362 """ 

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

364 

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

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

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

368 """ 

369 if isinstance(other, Offset2D): 

370 return self.__class__( 

371 self.xOffset - other.xOffset, 

372 self.yOffset - other.yOffset 

373 ) 

374 elif isinstance(other, tuple): 

375 return self.__class__( 

376 self.xOffset - other[0], 

377 self.yOffset - other[1] 

378 ) 

379 else: 

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

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

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

383 raise ex 

384 

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

386 """ 

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

388 

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

390 :returns: This 2D-point. 

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

392 """ 

393 if isinstance(other, Offset2D): 

394 self.xOffset -= other.xOffset 

395 self.yOffset -= other.yOffset 

396 elif isinstance(other, tuple): 

397 self.xOffset -= other[0] 

398 self.yOffset -= other[1] 

399 else: 

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

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

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

403 raise ex 

404 

405 return self 

406 

407 def __repr__(self) -> str: 

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

409 

410 def __str__(self) -> str: 

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

412 

413 

414@export 

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

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

417 

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

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

420 

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

422 """ 

423 Initializes a 2-dimensional size. 

424 

425 :param width: width in x-direction. 

426 :param height: height in y-direction. 

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

428 """ 

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

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

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

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

433 raise ex 

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

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

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

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

438 raise ex 

439 

440 self.width = width 

441 self.height = height 

442 

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

444 """ 

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

446 

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

448 """ 

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

450 

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

452 """ 

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

454 

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

456 """ 

457 return self.width, self.height 

458 

459 def __repr__(self) -> str: 

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

461 

462 def __str__(self) -> str: 

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

464 

465 

466@export 

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

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

469 

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

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

472 

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

474 """ 

475 Initializes a 2-dimensional segment. 

476 

477 :param start: Start point of the segment. 

478 :param end: End point of the segment. 

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

480 """ 

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

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

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

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

485 raise ex 

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

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

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

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

490 raise ex 

491 

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

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

494 

495 

496@export 

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

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

499 

500 @readonly 

501 def Length(self) -> float: 

502 """ 

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

504 

505 :return: Euclidean distance between start and end point 

506 """ 

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

508 

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

510 vectorA = self.ToOffset() 

511 vectorB = other.ToOffset() 

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

513 

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

515 

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

517 """ 

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

519 

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

521 """ 

522 return self.end - self.start 

523 

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

525 """ 

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

527 

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

529 """ 

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

531 

532 def __repr__(self) -> str: 

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

534 

535 def __str__(self) -> str: 

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