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

229 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-11 23:59 +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""" 

32An implementation of 3D cartesian data structures for Python. 

33 

34.. seealso:: 

35 

36 :mod:`pyTooling.Cartesian2D` 

37 |rarr| The same data structures in two dimensions. 

38 :mod:`pyTooling.Cartesian3D.Volumes` 

39 |rarr| Volumes built from these points and offsets. 

40""" 

41from __future__ import annotations 

42 

43from math import sqrt, acos 

44from typing import Union, Generic, Any, Self 

45 

46from pyTooling.Decorators import readonly, export 

47from pyTooling.MetaClasses import ExtendedType 

48from pyTooling.Common import getFullyQualifiedName 

49from pyTooling.Cartesian2D import Coordinate 

50 

51 

52@export 

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

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

55 

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

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

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

59 

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

61 """ 

62 Initializes a 3-dimensional point. 

63 

64 :param x: X-coordinate. 

65 :param y: Y-coordinate. 

66 :param z: Z-coordinate. 

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

68 """ 

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

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

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

72 raise ex 

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

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

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

76 raise ex 

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

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

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

80 raise ex 

81 

82 self.x = x 

83 self.y = y 

84 self.z = z 

85 

86 def Copy(self) -> Self: 

87 """ 

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

89 

90 :returns: Copy of this 3D-point. 

91 

92 .. seealso:: 

93 

94 :meth:`+ operator <__add__>` 

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

96 :meth:`- operator <__sub__>` 

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

98 """ 

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

100 

101 def ToTuple(self) -> tuple[Coordinate, Coordinate, Coordinate]: 

102 """ 

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

104 

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

106 """ 

107 return self.x, self.y, self.z 

108 

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

110 """ 

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

112 

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

114 :returns: A new 3D-point shifted by the 3D-offset. 

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

116 """ 

117 if isinstance(other, Offset3D): 

118 return self.__class__( 

119 self.x + other.xOffset, 

120 self.y + other.yOffset, 

121 self.z + other.zOffset 

122 ) 

123 elif isinstance(other, tuple): 

124 return self.__class__( 

125 self.x + other[0], 

126 self.y + other[1], 

127 self.z + other[2] 

128 ) 

129 else: 

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

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

132 raise ex 

133 

134 def __iadd__(self, other: Any) -> Self: 

135 """ 

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

137 

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

139 :returns: This 3D-point. 

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

141 """ 

142 if isinstance(other, Offset3D): 

143 self.x += other.xOffset 

144 self.y += other.yOffset 

145 self.z += other.zOffset 

146 elif isinstance(other, tuple): 

147 self.x += other[0] 

148 self.y += other[1] 

149 self.z += other[2] 

150 else: 

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

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

153 raise ex 

154 

155 return self 

156 

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

158 """ 

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

160 

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

162 :returns: A new 3D-offset representing the distance between these two points. 

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

164 """ 

165 if isinstance(other, Point3D): 

166 return Offset3D( 

167 self.x - other.x, 

168 self.y - other.y, 

169 self.z - other.z 

170 ) 

171 else: 

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

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

174 raise ex 

175 

176 def __isub__(self, other: Any) -> Self: 

177 """ 

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

179 

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

181 :returns: This 3D-point. 

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

183 """ 

184 if isinstance(other, Offset3D): 

185 self.x -= other.xOffset 

186 self.y -= other.yOffset 

187 self.z -= other.zOffset 

188 elif isinstance(other, tuple): 

189 self.x -= other[0] 

190 self.y -= other[1] 

191 self.z -= other[2] 

192 else: 

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

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

195 raise ex 

196 

197 return self 

198 

199 def __repr__(self) -> str: 

200 """ 

201 Returns the 3D point's string representation. 

202 

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

204 """ 

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

206 

207 def __str__(self) -> str: 

208 """ 

209 Returns the 3D point's string equivalent. 

210 

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

212 """ 

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

214 

215 

216@export 

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

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

219 

220 def __init__(self) -> None: 

221 """ 

222 Initializes a 3-dimensional origin. 

223 """ 

224 super().__init__(0, 0, 0) 

225 

226 def Copy(self) -> Self: 

227 """ 

228 An origin is a singular point, so it can't be copied. 

229 

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

231 """ 

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

233 

234 def __repr__(self) -> str: 

235 """ 

236 Returns the 3D origin's string representation. 

237 

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

239 """ 

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

241 

242 

243@export 

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

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

246 

247 xOffset: Coordinate #: The x-direction offset 

248 yOffset: Coordinate #: The y-direction offset 

249 zOffset: Coordinate #: The z-direction offset 

250 

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

252 """ 

253 Initializes a 3-dimensional offset. 

254 

255 :param xOffset: x-direction offset. 

256 :param yOffset: y-direction offset. 

257 :param zOffset: z-direction offset. 

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

259 """ 

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

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

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

263 raise ex 

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

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

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

267 raise ex 

268 if not isinstance(zOffset, (int, float)): 

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

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

271 raise ex 

272 

273 self.xOffset = xOffset 

274 self.yOffset = yOffset 

275 self.zOffset = zOffset 

276 

277 def Copy(self) -> Self: 

278 """ 

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

280 

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

282 

283 .. seealso:: 

284 

285 :meth:`+ operator <__add__>` 

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

287 :meth:`- operator <__sub__>` 

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

289 """ 

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

291 

292 def ToTuple(self) -> tuple[Coordinate, Coordinate, Coordinate]: 

293 """ 

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

295 

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

297 """ 

298 return self.xOffset, self.yOffset, self.zOffset 

299 

300 def __eq__(self, other: Any) -> bool: 

301 """ 

302 Compare two 3D-offsets for equality. 

303 

304 :param other: Parameter to compare against. 

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

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

307 """ 

308 if isinstance(other, Offset3D): 

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

310 elif isinstance(other, tuple): 

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

312 else: 

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

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

315 raise ex 

316 

317 def __ne__(self, other: Any) -> bool: 

318 """ 

319 Compare two 3D-offsets for inequality. 

320 

321 :param other: Parameter to compare against. 

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

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

324 """ 

325 return not self.__eq__(other) 

326 

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

328 """ 

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

330 

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

332 """ 

333 return self.__class__( 

334 -self.xOffset, 

335 -self.yOffset, 

336 -self.zOffset 

337 ) 

338 

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

340 """ 

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

342 

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

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

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

346 """ 

347 if isinstance(other, Offset3D): 

348 return self.__class__( 

349 self.xOffset + other.xOffset, 

350 self.yOffset + other.yOffset, 

351 self.zOffset + other.zOffset 

352 ) 

353 elif isinstance(other, tuple): 

354 return self.__class__( 

355 self.xOffset + other[0], 

356 self.yOffset + other[1], 

357 self.zOffset + other[2] 

358 ) 

359 else: 

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

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

362 raise ex 

363 

364 def __iadd__(self, other: Any) -> Self: 

365 """ 

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

367 

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

369 :returns: This 3D-point. 

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

371 """ 

372 if isinstance(other, Offset3D): 

373 self.xOffset += other.xOffset 

374 self.yOffset += other.yOffset 

375 self.zOffset += other.zOffset 

376 elif isinstance(other, tuple): 

377 self.xOffset += other[0] 

378 self.yOffset += other[1] 

379 self.zOffset += other[2] 

380 else: 

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

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

383 raise ex 

384 

385 return self 

386 

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

388 """ 

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

390 

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

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

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

394 """ 

395 if isinstance(other, Offset3D): 

396 return self.__class__( 

397 self.xOffset - other.xOffset, 

398 self.yOffset - other.yOffset, 

399 self.zOffset - other.zOffset 

400 ) 

401 elif isinstance(other, tuple): 

402 return self.__class__( 

403 self.xOffset - other[0], 

404 self.yOffset - other[1], 

405 self.zOffset - other[2] 

406 ) 

407 else: 

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

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

410 raise ex 

411 

412 def __isub__(self, other: Any) -> Self: 

413 """ 

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

415 

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

417 :returns: This 3D-point. 

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

419 """ 

420 if isinstance(other, Offset3D): 

421 self.xOffset -= other.xOffset 

422 self.yOffset -= other.yOffset 

423 self.zOffset -= other.zOffset 

424 elif isinstance(other, tuple): 

425 self.xOffset -= other[0] 

426 self.yOffset -= other[1] 

427 self.zOffset -= other[2] 

428 else: 

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

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

431 raise ex 

432 

433 return self 

434 

435 def __repr__(self) -> str: 

436 """ 

437 Returns the 3D offset's string representation. 

438 

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

440 """ 

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

442 

443 def __str__(self) -> str: 

444 """ 

445 Returns the 3D offset's string equivalent. 

446 

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

448 """ 

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

450 

451 

452@export 

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

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

455 

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

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

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

459 

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

461 """ 

462 Initializes a 2-dimensional size. 

463 

464 :param width: width in x-direction. 

465 :param height: height in y-direction. 

466 :param depth: depth in z-direction. 

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

468 """ 

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

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

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

472 raise ex 

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

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

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

476 raise ex 

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

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

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

480 raise ex 

481 

482 self.width = width 

483 self.height = height 

484 self.depth = depth 

485 

486 def Copy(self) -> Self: 

487 """ 

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

489 

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

491 """ 

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

493 

494 def ToTuple(self) -> tuple[Coordinate, Coordinate, Coordinate]: 

495 """ 

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

497 

498 :returns: ``(width, height, depth)`` tuple. 

499 """ 

500 return self.width, self.height, self.depth 

501 

502 def __repr__(self) -> str: 

503 """ 

504 Returns the 3D size's string representation. 

505 

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

507 """ 

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

509 

510 def __str__(self) -> str: 

511 """ 

512 Returns the 3D size's string equivalent. 

513 

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

515 """ 

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

517 

518 

519@export 

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

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

522 

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

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

525 

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

527 """ 

528 Initializes a 3-dimensional segment. 

529 

530 :param start: Start point of the segment. 

531 :param end: End point of the segment. 

532 :param copyPoints: Optional, if ``True``, the given points are copied instead of referenced. 

533 :raises TypeError: If start/end is not of type :class:`Point3D`. 

534 """ 

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

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

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

538 raise ex 

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

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

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

542 raise ex 

543 

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

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

546 

547 

548@export 

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

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

551 

552 @readonly 

553 def Length(self) -> float: 

554 """ 

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

556 

557 :returns: Euclidean distance between start and end point 

558 """ 

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

560 

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

562 """ 

563 Compute the angle between this line segment and another one. 

564 

565 :param other: The second line segment. 

566 :returns: The angle in radians. 

567 """ 

568 vectorA = self.ToOffset() 

569 vectorB = other.ToOffset() 

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

571 

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

573 

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

575 """ 

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

577 

578 :returns: 3D-offset as :class:`Offset3D` 

579 """ 

580 return self.end - self.start 

581 

582 def ToTuple(self) -> tuple[tuple[Coordinate, Coordinate, Coordinate], tuple[Coordinate, Coordinate, Coordinate]]: 

583 """ 

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

585 

586 :returns: ``((x1, y1, z1), (x2, y2, z2))`` tuple. 

587 """ 

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

589 

590 def __repr__(self) -> str: 

591 """ 

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

593 

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

595 """ 

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

597 

598 def __str__(self) -> str: 

599 """ 

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

601 

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

603 """ 

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