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
« 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.
34.. seealso::
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
43from math import sqrt, acos
44from typing import Union, Generic, Any, Self
46from pyTooling.Decorators import readonly, export
47from pyTooling.MetaClasses import ExtendedType
48from pyTooling.Common import getFullyQualifiedName
49from pyTooling.Cartesian2D import Coordinate
52@export
53class Point3D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
54 """An implementation of a 3D cartesian point."""
56 x: Coordinate #: The x-direction coordinate.
57 y: Coordinate #: The y-direction coordinate.
58 z: Coordinate #: The z-direction coordinate.
60 def __init__(self, x: Coordinate, y: Coordinate, z: Coordinate) -> None:
61 """
62 Initializes a 3-dimensional point.
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
82 self.x = x
83 self.y = y
84 self.z = z
86 def Copy(self) -> Self:
87 """
88 Create a new 3D-point as a copy of this 3D point.
90 :returns: Copy of this 3D-point.
92 .. seealso::
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)
101 def ToTuple(self) -> tuple[Coordinate, Coordinate, Coordinate]:
102 """
103 Convert this 3D-Point to a simple 3-element tuple.
105 :returns: ``(x, y, z)`` tuple.
106 """
107 return self.x, self.y, self.z
109 def __add__(self, other: Any) -> Point3D[Coordinate]:
110 """
111 Adds a 3D-offset to this 3D-point and creates a new 3D-point.
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
134 def __iadd__(self, other: Any) -> Self:
135 """
136 Adds a 3D-offset to this 3D-point (inplace).
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
155 return self
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.
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
176 def __isub__(self, other: Any) -> Self:
177 """
178 Subtracts a 3D-offset to this 3D-point (inplace).
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
197 return self
199 def __repr__(self) -> str:
200 """
201 Returns the 3D point's string representation.
203 :returns: The string representation of the 3D point.
204 """
205 return f"Point3D({self.x}, {self.y}, {self.z})"
207 def __str__(self) -> str:
208 """
209 Returns the 3D point's string equivalent.
211 :returns: The string equivalent of the 3D point.
212 """
213 return f"({self.x}, {self.y}, {self.z})"
216@export
217class Origin3D(Point3D[Coordinate], Generic[Coordinate]):
218 """An implementation of a 3D cartesian origin."""
220 def __init__(self) -> None:
221 """
222 Initializes a 3-dimensional origin.
223 """
224 super().__init__(0, 0, 0)
226 def Copy(self) -> Self:
227 """
228 An origin is a singular point, so it can't be copied.
230 :raises RuntimeError: Because an origin can't be copied.
231 """
232 raise RuntimeError(f"An origin can't be copied.")
234 def __repr__(self) -> str:
235 """
236 Returns the 3D origin's string representation.
238 :returns: The string representation of the 3D origin.
239 """
240 return f"Origin3D({self.x}, {self.y}, {self.z})"
243@export
244class Offset3D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
245 """An implementation of a 3D cartesian offset."""
247 xOffset: Coordinate #: The x-direction offset
248 yOffset: Coordinate #: The y-direction offset
249 zOffset: Coordinate #: The z-direction offset
251 def __init__(self, xOffset: Coordinate, yOffset: Coordinate, zOffset: Coordinate) -> None:
252 """
253 Initializes a 3-dimensional offset.
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
273 self.xOffset = xOffset
274 self.yOffset = yOffset
275 self.zOffset = zOffset
277 def Copy(self) -> Self:
278 """
279 Create a new 3D-offset as a copy of this 3D-offset.
281 :returns: Copy of this 3D-offset.
283 .. seealso::
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)
292 def ToTuple(self) -> tuple[Coordinate, Coordinate, Coordinate]:
293 """
294 Convert this 3D-offset to a simple 3-element tuple.
296 :returns: ``(x, y, z)`` tuple.
297 """
298 return self.xOffset, self.yOffset, self.zOffset
300 def __eq__(self, other: Any) -> bool:
301 """
302 Compare two 3D-offsets for equality.
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
317 def __ne__(self, other: Any) -> bool:
318 """
319 Compare two 3D-offsets for inequality.
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)
327 def __neg__(self) -> Offset3D[Coordinate]:
328 """
329 Negate all components of this 3D-offset and create a new 3D-offset.
331 :returns: 3D-offset with negated offset components.
332 """
333 return self.__class__(
334 -self.xOffset,
335 -self.yOffset,
336 -self.zOffset
337 )
339 def __add__(self, other: Any) -> Offset3D[Coordinate]:
340 """
341 Adds a 3D-offset to this 3D-offset and creates a new 3D-offset.
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
364 def __iadd__(self, other: Any) -> Self:
365 """
366 Adds a 3D-offset to this 3D-offset (inplace).
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
385 return self
387 def __sub__(self, other: Any) -> Offset3D[Coordinate]:
388 """
389 Subtracts a 3D-offset from this 3D-offset and creates a new 3D-offset.
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
412 def __isub__(self, other: Any) -> Self:
413 """
414 Subtracts a 3D-offset from this 3D-offset (inplace).
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
433 return self
435 def __repr__(self) -> str:
436 """
437 Returns the 3D offset's string representation.
439 :returns: The string representation of the 3D offset.
440 """
441 return f"Offset3D({self.xOffset}, {self.yOffset}, {self.zOffset})"
443 def __str__(self) -> str:
444 """
445 Returns the 3D offset's string equivalent.
447 :returns: The string equivalent of the 3D offset.
448 """
449 return f"({self.xOffset}, {self.yOffset}, {self.zOffset})"
452@export
453class Size3D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
454 """An implementation of a 3D cartesian size."""
456 width: Coordinate #: width in x-direction.
457 height: Coordinate #: height in y-direction.
458 depth: Coordinate #: depth in z-direction.
460 def __init__(self, width: Coordinate, height: Coordinate, depth: Coordinate) -> None:
461 """
462 Initializes a 2-dimensional size.
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
482 self.width = width
483 self.height = height
484 self.depth = depth
486 def Copy(self) -> Self:
487 """
488 Create a new 3D-size as a copy of this 3D-size.
490 :returns: Copy of this 3D-size.
491 """
492 return self.__class__(self.width, self.height, self.depth)
494 def ToTuple(self) -> tuple[Coordinate, Coordinate, Coordinate]:
495 """
496 Convert this 3D-size to a simple 3-element tuple.
498 :returns: ``(width, height, depth)`` tuple.
499 """
500 return self.width, self.height, self.depth
502 def __repr__(self) -> str:
503 """
504 Returns the 3D size's string representation.
506 :returns: The string representation of the 3D size.
507 """
508 return f"Size3D({self.width}, {self.height}, {self.depth})"
510 def __str__(self) -> str:
511 """
512 Returns the 3D size's string equivalent.
514 :returns: The string equivalent of the 3D size.
515 """
516 return f"({self.width}, {self.height}, {self.depth})"
519@export
520class Segment3D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
521 """An implementation of a 3D cartesian segment."""
523 start: Point3D[Coordinate] #: Start point of a segment.
524 end: Point3D[Coordinate] #: End point of a segment.
526 def __init__(self, start: Point3D[Coordinate], end: Point3D[Coordinate], copyPoints: bool = True) -> None:
527 """
528 Initializes a 3-dimensional segment.
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
544 self.start = start.Copy() if copyPoints else start
545 self.end = end.Copy() if copyPoints else end
548@export
549class LineSegment3D(Segment3D[Coordinate], Generic[Coordinate]):
550 """An implementation of a 3D cartesian line segment."""
552 @readonly
553 def Length(self) -> float:
554 """
555 Read-only property to return the Euclidean distance between start and end point.
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)
561 def AngleTo(self, other: LineSegment3D[Coordinate]) -> float:
562 """
563 Compute the angle between this line segment and another one.
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
572 return acos(scalarProductAB / (abs(self.Length) * abs(other.Length)))
574 def ToOffset(self) -> Offset3D[Coordinate]:
575 """
576 Convert this 3D line segment to a 3D-offset.
578 :returns: 3D-offset as :class:`Offset3D`
579 """
580 return self.end - self.start
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.
586 :returns: ``((x1, y1, z1), (x2, y2, z2))`` tuple.
587 """
588 return self.start.ToTuple(), self.end.ToTuple()
590 def __repr__(self) -> str:
591 """
592 Returns the 3D line segment's string representation.
594 :returns: The string representation of the 3D line segment.
595 """
596 return f"LineSegment3D({self.start}, {self.end})"
598 def __str__(self) -> str:
599 """
600 Returns the 3D line segment's string equivalent.
602 :returns: The string equivalent of the 3D line segment.
603 """
604 return f"({self.start} → {self.end})"