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

228 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 3D cartesian data structures for Python.""" 

32 

33from math import sqrt, acos 

34from typing import Union, Generic, Any, Tuple 

35 

36from pyTooling.Decorators import readonly, export 

37from pyTooling.MetaClasses import ExtendedType 

38from pyTooling.Common import getFullyQualifiedName 

39from pyTooling.Cartesian2D import Coordinate 

40 

41 

42@export 

43class Point3D(Generic[Coordinate], metaclass=ExtendedType, slots=True): 

44 """An implementation of a 3D cartesian point.""" 

45 

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

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

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

49 

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

51 """ 

52 Initializes a 3-dimensional point. 

53 

54 :param x: X-coordinate. 

55 :param y: Y-coordinate. 

56 :param z: Z-coordinate. 

57 :raises TypeError: If x/y/z-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 if not isinstance(z, (int, float)): 

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

69 ex.add_note(f"Got type '{getFullyQualifiedName(z)}'.") 

70 raise ex 

71 

72 self.x = x 

73 self.y = y 

74 self.z = z 

75 

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

77 """ 

78 Create a new 3D-point as a copy of this 3D point. 

79 

80 :return: Copy of this 3D-point. 

81 

82 .. seealso:: 

83 

84 :meth:`+ operator <__add__>` 

85 Create a new 3D-point moved by a positive 3D-offset. 

86 :meth:`- operator <__sub__>` 

87 Create a new 3D-point moved by a negative 3D-offset. 

88 """ 

89 return self.__class__(self.x, self.y, self.z) 

90 

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

92 """ 

93 Convert this 3D-Point to a simple 3-element tuple. 

94 

95 :return: ``(x, y, z)`` tuple. 

96 """ 

97 return self.x, self.y, self.z 

98 

99 def __add__(self, other: Any) -> "Point3D[Coordinate]": 

100 """ 

101 Adds a 3D-offset to this 3D-point and creates a new 3D-point. 

102 

103 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`. 

104 :return: A new 3D-point shifted by the 3D-offset. 

105 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`. 

106 """ 

107 if isinstance(other, Offset3D): 

108 return self.__class__( 

109 self.x + other.xOffset, 

110 self.y + other.yOffset, 

111 self.z + other.zOffset 

112 ) 

113 elif isinstance(other, tuple): 

114 return self.__class__( 

115 self.x + other[0], 

116 self.y + other[1], 

117 self.z + other[2] 

118 ) 

119 else: 

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

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

122 raise ex 

123 

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

125 """ 

126 Adds a 3D-offset to this 3D-point (inplace). 

127 

128 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`. 

129 :return: This 3D-point. 

130 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`. 

131 """ 

132 if isinstance(other, Offset3D): 

133 self.x += other.xOffset 

134 self.y += other.yOffset 

135 self.z += other.zOffset 

136 elif isinstance(other, tuple): 

137 self.x += other[0] 

138 self.y += other[1] 

139 self.z += other[2] 

140 else: 

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

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

143 raise ex 

144 

145 return self 

146 

147 def __sub__(self, other: Any) -> Union["Offset3D[Coordinate]", "Point3D[Coordinate]"]: 

148 """ 

149 Subtract two 3D-Points from each other and create a new 3D-offset. 

150 

151 :param other: A 3D-point as :class:`Point3D`. 

152 :return: A new 3D-offset representing the distance between these two points. 

153 :raises TypeError: If parameter 'other' is not a :class:`Point3D`. 

154 """ 

155 if isinstance(other, Point3D): 

156 return Offset3D( 

157 self.x - other.x, 

158 self.y - other.y, 

159 self.z - other.z 

160 ) 

161 else: 

162 ex = TypeError(f"Parameter 'other' is not of type Point3D.") 

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

164 raise ex 

165 

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

167 """ 

168 Subtracts a 3D-offset to this 3D-point (inplace). 

169 

170 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`. 

171 :return: This 3D-point. 

172 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`. 

173 """ 

174 if isinstance(other, Offset3D): 

175 self.x -= other.xOffset 

176 self.y -= other.yOffset 

177 self.z -= other.zOffset 

178 elif isinstance(other, tuple): 

179 self.x -= other[0] 

180 self.y -= other[1] 

181 self.z -= other[2] 

182 else: 

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

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

185 raise ex 

186 

187 return self 

188 

189 def __repr__(self) -> str: 

190 """ 

191 Returns the 3D point's string representation. 

192 

193 :returns: The string representation of the 3D point. 

194 """ 

195 return f"Point3D({self.x}, {self.y}, {self.z})" 

196 

197 def __str__(self) -> str: 

198 """ 

199 Returns the 3D point's string equivalent. 

200 

201 :returns: The string equivalent of the 3D point. 

202 """ 

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

204 

205 

206@export 

207class Origin3D(Point3D[Coordinate], Generic[Coordinate]): 

208 """An implementation of a 3D cartesian origin.""" 

209 

210 def __init__(self) -> None: 

211 """ 

212 Initializes a 3-dimensional origin. 

213 """ 

214 super().__init__(0, 0, 0) 

215 

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

217 """ 

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

219 """ 

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

221 

222 def __repr__(self) -> str: 

223 """ 

224 Returns the 3D origin's string representation. 

225 

226 :returns: The string representation of the 3D origin. 

227 """ 

228 return f"Origin3D({self.x}, {self.y}, {self.z})" 

229 

230 

231@export 

232class Offset3D(Generic[Coordinate], metaclass=ExtendedType, slots=True): 

233 """An implementation of a 3D cartesian offset.""" 

234 

235 xOffset: Coordinate #: The x-direction offset 

236 yOffset: Coordinate #: The y-direction offset 

237 zOffset: Coordinate #: The z-direction offset 

238 

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

240 """ 

241 Initializes a 3-dimensional offset. 

242 

243 :param xOffset: x-direction offset. 

244 :param yOffset: y-direction offset. 

245 :param zOffset: z-direction offset. 

246 :raises TypeError: If x/y/z-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 if not isinstance(zOffset, (int, float)): 

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

258 ex.add_note(f"Got type '{getFullyQualifiedName(zOffset)}'.") 

259 raise ex 

260 

261 self.xOffset = xOffset 

262 self.yOffset = yOffset 

263 self.zOffset = zOffset 

264 

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

266 """ 

267 Create a new 3D-offset as a copy of this 3D-offset. 

268 

269 :returns: Copy of this 3D-offset. 

270 

271 .. seealso:: 

272 

273 :meth:`+ operator <__add__>` 

274 Create a new 3D-offset moved by a positive 3D-offset. 

275 :meth:`- operator <__sub__>` 

276 Create a new 3D-offset moved by a negative 3D-offset. 

277 """ 

278 return self.__class__(self.xOffset, self.yOffset, self.zOffset) 

279 

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

281 """ 

282 Convert this 3D-offset to a simple 3-element tuple. 

283 

284 :returns: ``(x, y, z)`` tuple. 

285 """ 

286 return self.xOffset, self.yOffset, self.zOffset 

287 

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

289 """ 

290 Compare two 3D-offsets for equality. 

291 

292 :param other: Parameter to compare against. 

293 :returns: ``True``, if both 3D-offsets are equal. 

294 :raises TypeError: If parameter ``other`` is not of type :class:`Offset3D` or :class:`tuple`. 

295 """ 

296 if isinstance(other, Offset3D): 

297 return self.xOffset == other.xOffset and self.yOffset == other.yOffset and self.zOffset == other.zOffset 

298 elif isinstance(other, tuple): 

299 return self.xOffset == other[0] and self.yOffset == other[1] and self.zOffset == other[2] 

300 else: 

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

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

303 raise ex 

304 

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

306 """ 

307 Compare two 3D-offsets for inequality. 

308 

309 :param other: Parameter to compare against. 

310 :returns: ``True``, if both 3D-offsets are unequal. 

311 :raises TypeError: If parameter ``other`` is not of type :class:`Offset3D` or :class:`tuple`. 

312 """ 

313 return not self.__eq__(other) 

314 

315 def __neg__(self) -> "Offset3D[Coordinate]": 

316 """ 

317 Negate all components of this 3D-offset and create a new 3D-offset. 

318 

319 :returns: 3D-offset with negated offset components. 

320 """ 

321 return self.__class__( 

322 -self.xOffset, 

323 -self.yOffset, 

324 -self.zOffset 

325 ) 

326 

327 def __add__(self, other: Any) -> "Offset3D[Coordinate]": 

328 """ 

329 Adds a 3D-offset to this 3D-offset and creates a new 3D-offset. 

330 

331 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`. 

332 :returns: A new 3D-offset extended by the 3D-offset. 

333 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`. 

334 """ 

335 if isinstance(other, Offset3D): 

336 return self.__class__( 

337 self.xOffset + other.xOffset, 

338 self.yOffset + other.yOffset, 

339 self.zOffset + other.zOffset 

340 ) 

341 elif isinstance(other, tuple): 

342 return self.__class__( 

343 self.xOffset + other[0], 

344 self.yOffset + other[1], 

345 self.zOffset + other[2] 

346 ) 

347 else: 

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

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

350 raise ex 

351 

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

353 """ 

354 Adds a 3D-offset to this 3D-offset (inplace). 

355 

356 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`. 

357 :returns: This 3D-point. 

358 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`. 

359 """ 

360 if isinstance(other, Offset3D): 

361 self.xOffset += other.xOffset 

362 self.yOffset += other.yOffset 

363 self.zOffset += other.zOffset 

364 elif isinstance(other, tuple): 

365 self.xOffset += other[0] 

366 self.yOffset += other[1] 

367 self.zOffset += other[2] 

368 else: 

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

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

371 raise ex 

372 

373 return self 

374 

375 def __sub__(self, other: Any) -> "Offset3D[Coordinate]": 

376 """ 

377 Subtracts a 3D-offset from this 3D-offset and creates a new 3D-offset. 

378 

379 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`. 

380 :returns: A new 3D-offset reduced by the 3D-offset. 

381 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`. 

382 """ 

383 if isinstance(other, Offset3D): 

384 return self.__class__( 

385 self.xOffset - other.xOffset, 

386 self.yOffset - other.yOffset, 

387 self.zOffset - other.zOffset 

388 ) 

389 elif isinstance(other, tuple): 

390 return self.__class__( 

391 self.xOffset - other[0], 

392 self.yOffset - other[1], 

393 self.zOffset - other[2] 

394 ) 

395 else: 

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

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

398 raise ex 

399 

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

401 """ 

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

403 

404 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`. 

405 :returns: This 3D-point. 

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

407 """ 

408 if isinstance(other, Offset3D): 

409 self.xOffset -= other.xOffset 

410 self.yOffset -= other.yOffset 

411 self.zOffset -= other.zOffset 

412 elif isinstance(other, tuple): 

413 self.xOffset -= other[0] 

414 self.yOffset -= other[1] 

415 self.zOffset -= other[2] 

416 else: 

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

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

419 raise ex 

420 

421 return self 

422 

423 def __repr__(self) -> str: 

424 """ 

425 Returns the 3D offset's string representation. 

426 

427 :returns: The string representation of the 3D offset. 

428 """ 

429 return f"Offset3D({self.xOffset}, {self.yOffset}, {self.zOffset})" 

430 

431 def __str__(self) -> str: 

432 """ 

433 Returns the 3D offset's string equivalent. 

434 

435 :returns: The string equivalent of the 3D offset. 

436 """ 

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

438 

439 

440@export 

441class Size3D(Generic[Coordinate], metaclass=ExtendedType, slots=True): 

442 """An implementation of a 3D cartesian size.""" 

443 

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

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

446 depth: Coordinate #: depth in z-direction. 

447 

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

449 """ 

450 Initializes a 2-dimensional size. 

451 

452 :param width: width in x-direction. 

453 :param height: height in y-direction. 

454 :param depth: depth in z-direction. 

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

456 """ 

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

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

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

460 raise ex 

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

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

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

464 raise ex 

465 if not isinstance(depth, (int, float)): 

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

467 ex.add_note(f"Got type '{getFullyQualifiedName(depth)}'.") 

468 raise ex 

469 

470 self.width = width 

471 self.height = height 

472 self.depth = depth 

473 

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

475 """ 

476 Create a new 3D-size as a copy of this 3D-size. 

477 

478 :returns: Copy of this 3D-size. 

479 """ 

480 return self.__class__(self.width, self.height, self.depth) 

481 

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

483 """ 

484 Convert this 3D-size to a simple 3-element tuple. 

485 

486 :return: ``(width, height, depth)`` tuple. 

487 """ 

488 return self.width, self.height, self.depth 

489 

490 def __repr__(self) -> str: 

491 """ 

492 Returns the 3D size's string representation. 

493 

494 :returns: The string representation of the 3D size. 

495 """ 

496 return f"Size3D({self.width}, {self.height}, {self.depth})" 

497 

498 def __str__(self) -> str: 

499 """ 

500 Returns the 3D size's string equivalent. 

501 

502 :returns: The string equivalent of the 3D size. 

503 """ 

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

505 

506 

507@export 

508class Segment3D(Generic[Coordinate], metaclass=ExtendedType, slots=True): 

509 """An implementation of a 3D cartesian segment.""" 

510 

511 start: Point3D[Coordinate] #: Start point of a segment. 

512 end: Point3D[Coordinate] #: End point of a segment. 

513 

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

515 """ 

516 Initializes a 3-dimensional segment. 

517 

518 :param start: Start point of the segment. 

519 :param end: End point of the segment. 

520 :raises TypeError: If start/end is not of type Point3D. 

521 """ 

522 if not isinstance(start, Point3D): 522 ↛ 523line 522 didn't jump to line 523 because the condition on line 522 was never true

523 ex = TypeError(f"Parameter 'start' is not of type Point3D.") 

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

525 raise ex 

526 if not isinstance(end, Point3D): 526 ↛ 527line 526 didn't jump to line 527 because the condition on line 526 was never true

527 ex = TypeError(f"Parameter 'end' is not of type Point3D.") 

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

529 raise ex 

530 

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

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

533 

534 

535@export 

536class LineSegment3D(Segment3D[Coordinate], Generic[Coordinate]): 

537 """An implementation of a 3D cartesian line segment.""" 

538 

539 @readonly 

540 def Length(self) -> float: 

541 """ 

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

543 

544 :return: Euclidean distance between start and end point 

545 """ 

546 return sqrt((self.end.x - self.start.x) ** 2 + (self.end.y - self.start.y) ** 2 + (self.end.z - self.start.z) ** 2) 

547 

548 def AngleTo(self, other: "LineSegment3D[Coordinate]") -> float: 

549 vectorA = self.ToOffset() 

550 vectorB = other.ToOffset() 

551 scalarProductAB = vectorA.xOffset * vectorB.xOffset + vectorA.yOffset * vectorB.yOffset + vectorA.zOffset * vectorB.zOffset 

552 

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

554 

555 def ToOffset(self) -> Offset3D[Coordinate]: 

556 """ 

557 Convert this 3D line segment to a 3D-offset. 

558 

559 :return: 3D-offset as :class:`Offset3D` 

560 """ 

561 return self.end - self.start 

562 

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

564 """ 

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

566 

567 :return: ``((x1, y1, z1), (x2, y2, z2))`` tuple. 

568 """ 

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

570 

571 def __repr__(self) -> str: 

572 """ 

573 Returns the 3D line segment's string representation. 

574 

575 :returns: The string representation of the 3D line segment. 

576 """ 

577 return f"LineSegment3D({self.start}, {self.end})" 

578 

579 def __str__(self) -> str: 

580 """ 

581 Returns the 3D line segment's string equivalent. 

582 

583 :returns: The string equivalent of the 3D line segment. 

584 """ 

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