Coverage for pyTooling/Cartesian3D/__init__.py: 94%
230 statements
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-14 22:21 +0000
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-14 22:21 +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 3D cartesian data structures for Python."""
33from math import sqrt, acos
34from typing import Union, Generic, Any, Tuple
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
41 from pyTooling.Cartesian2D import Coordinate
42except (ImportError, ModuleNotFoundError): # pragma: no cover
43 print("[pyTooling.Cartesian2D] Could not import from 'pyTooling.*'!")
45 try:
46 from Decorators import readonly, export
47 from Exceptions import ToolingException
48 from MetaClasses import ExtendedType
49 from Common import getFullyQualifiedName
50 from Cartesian2D import Coordinate
51 except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover
52 print("[pyTooling.Cartesian2D] Could not import directly!")
53 raise ex
56@export
57class Point3D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
58 """An implementation of a 3D cartesian point."""
60 x: Coordinate #: The x-direction coordinate.
61 y: Coordinate #: The y-direction coordinate.
62 z: Coordinate #: The z-direction coordinate.
64 def __init__(self, x: Coordinate, y: Coordinate, z: Coordinate) -> None:
65 """
66 Initializes a 3-dimensional point.
68 :param x: X-coordinate.
69 :param y: Y-coordinate.
70 :param z: Z-coordinate.
71 :raises TypeError: If x/y/z-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 ex.add_note(f"Got type '{getFullyQualifiedName(x)}'.")
76 raise ex
77 if not isinstance(y, (int, float)):
78 ex = TypeError(f"Parameter 'y' is not of type integer or float.")
79 ex.add_note(f"Got type '{getFullyQualifiedName(y)}'.")
80 raise ex
81 if not isinstance(z, (int, float)):
82 ex = TypeError(f"Parameter 'z' is not of type integer or float.")
83 ex.add_note(f"Got type '{getFullyQualifiedName(z)}'.")
84 raise ex
86 self.x = x
87 self.y = y
88 self.z = z
90 def Copy(self) -> "Point3D[Coordinate]": # TODO: Python 3.11: -> Self:
91 """
92 Create a new 3D-point as a copy of this 3D point.
94 :return: Copy of this 3D-point.
96 .. seealso::
98 :meth:`+ operator <__add__>`
99 Create a new 3D-point moved by a positive 3D-offset.
100 :meth:`- operator <__sub__>`
101 Create a new 3D-point moved by a negative 3D-offset.
102 """
103 return self.__class__(self.x, self.y, self.z)
105 def ToTuple(self) -> Tuple[Coordinate, Coordinate, Coordinate]:
106 """
107 Convert this 3D-Point to a simple 3-element tuple.
109 :return: ``(x, y, z)`` tuple.
110 """
111 return self.x, self.y, self.z
113 def __add__(self, other: Any) -> "Point3D[Coordinate]":
114 """
115 Adds a 3D-offset to this 3D-point and creates a new 3D-point.
117 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`.
118 :return: A new 3D-point shifted by the 3D-offset.
119 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
120 """
121 if isinstance(other, Offset3D):
122 return self.__class__(
123 self.x + other.xOffset,
124 self.y + other.yOffset,
125 self.z + other.zOffset
126 )
127 elif isinstance(other, tuple):
128 return self.__class__(
129 self.x + other[0],
130 self.y + other[1],
131 self.z + other[2]
132 )
133 else:
134 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
135 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
136 raise ex
138 def __iadd__(self, other: Any) -> "Point3D[Coordinate]": # TODO: Python 3.11: -> Self:
139 """
140 Adds a 3D-offset to this 3D-point (inplace).
142 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`.
143 :return: This 3D-point.
144 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
145 """
146 if isinstance(other, Offset3D):
147 self.x += other.xOffset
148 self.y += other.yOffset
149 self.z += other.zOffset
150 elif isinstance(other, tuple):
151 self.x += other[0]
152 self.y += other[1]
153 self.z += other[2]
154 else:
155 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
156 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
157 raise ex
159 return self
161 def __sub__(self, other: Any) -> Union["Offset3D[Coordinate]", "Point3D[Coordinate]"]:
162 """
163 Subtract two 3D-Points from each other and create a new 3D-offset.
165 :param other: A 3D-point as :class:`Point3D`.
166 :return: A new 3D-offset representing the distance between these two points.
167 :raises TypeError: If parameter 'other' is not a :class:`Point3D`.
168 """
169 if isinstance(other, Point3D):
170 return Offset3D(
171 self.x - other.x,
172 self.y - other.y,
173 self.z - other.z
174 )
175 else:
176 ex = TypeError(f"Parameter 'other' is not of type Point3D.")
177 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
178 raise ex
180 def __isub__(self, other: Any) -> "Point3D[Coordinate]": # TODO: Python 3.11: -> Self:
181 """
182 Subtracts a 3D-offset to this 3D-point (inplace).
184 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`.
185 :return: This 3D-point.
186 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
187 """
188 if isinstance(other, Offset3D):
189 self.x -= other.xOffset
190 self.y -= other.yOffset
191 self.z -= other.zOffset
192 elif isinstance(other, tuple):
193 self.x -= other[0]
194 self.y -= other[1]
195 self.z -= other[2]
196 else:
197 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
198 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
199 raise ex
201 return self
203 def __repr__(self) -> str:
204 """
205 Returns the 3D point's string representation.
207 :returns: The string representation of the 3D point.
208 """
209 return f"Point3D({self.x}, {self.y}, {self.z})"
211 def __str__(self) -> str:
212 """
213 Returns the 3D point's string equivalent.
215 :returns: The string equivalent of the 3D point.
216 """
217 return f"({self.x}, {self.y}, {self.z})"
220@export
221class Origin3D(Point3D[Coordinate], Generic[Coordinate]):
222 """An implementation of a 3D cartesian origin."""
224 def __init__(self) -> None:
225 """
226 Initializes a 3-dimensional origin.
227 """
228 super().__init__(0, 0, 0)
230 def Copy(self) -> "Origin3D[Coordinate]": # TODO: Python 3.11: -> Self:
231 """
232 :raises RuntimeError: Because an origin can't be copied.
233 """
234 raise RuntimeError(f"An origin can't be copied.")
236 def __repr__(self) -> str:
237 """
238 Returns the 3D origin's string representation.
240 :returns: The string representation of the 3D origin.
241 """
242 return f"Origin3D({self.x}, {self.y}, {self.z})"
245@export
246class Offset3D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
247 """An implementation of a 3D cartesian offset."""
249 xOffset: Coordinate #: The x-direction offset
250 yOffset: Coordinate #: The y-direction offset
251 zOffset: Coordinate #: The z-direction offset
253 def __init__(self, xOffset: Coordinate, yOffset: Coordinate, zOffset: Coordinate) -> None:
254 """
255 Initializes a 3-dimensional offset.
257 :param xOffset: x-direction offset.
258 :param yOffset: y-direction offset.
259 :param zOffset: z-direction offset.
260 :raises TypeError: If x/y/z-offset is not of type integer or float.
261 """
262 if not isinstance(xOffset, (int, float)):
263 ex = TypeError(f"Parameter 'xOffset' is not of type integer or float.")
264 ex.add_note(f"Got type '{getFullyQualifiedName(xOffset)}'.")
265 raise ex
266 if not isinstance(yOffset, (int, float)):
267 ex = TypeError(f"Parameter 'yOffset' is not of type integer or float.")
268 ex.add_note(f"Got type '{getFullyQualifiedName(yOffset)}'.")
269 raise ex
270 if not isinstance(zOffset, (int, float)):
271 ex = TypeError(f"Parameter 'zOffset' is not of type integer or float.")
272 ex.add_note(f"Got type '{getFullyQualifiedName(zOffset)}'.")
273 raise ex
275 self.xOffset = xOffset
276 self.yOffset = yOffset
277 self.zOffset = zOffset
279 def Copy(self) -> "Offset3D[Coordinate]": # TODO: Python 3.11: -> Self:
280 """
281 Create a new 3D-offset as a copy of this 3D-offset.
283 :returns: Copy of this 3D-offset.
285 .. seealso::
287 :meth:`+ operator <__add__>`
288 Create a new 3D-offset moved by a positive 3D-offset.
289 :meth:`- operator <__sub__>`
290 Create a new 3D-offset moved by a negative 3D-offset.
291 """
292 return self.__class__(self.xOffset, self.yOffset, self.zOffset)
294 def ToTuple(self) -> Tuple[Coordinate, Coordinate, Coordinate]:
295 """
296 Convert this 3D-offset to a simple 3-element tuple.
298 :returns: ``(x, y, z)`` tuple.
299 """
300 return self.xOffset, self.yOffset, self.zOffset
302 def __eq__(self, other) -> bool:
303 """
304 Compare two 3D-offsets for equality.
306 :param other: Parameter to compare against.
307 :returns: ``True``, if both 3D-offsets are equal.
308 :raises TypeError: If parameter ``other`` is not of type :class:`Offset3D` or :class:`tuple`.
309 """
310 if isinstance(other, Offset3D):
311 return self.xOffset == other.xOffset and self.yOffset == other.yOffset and self.zOffset == other.zOffset
312 elif isinstance(other, tuple):
313 return self.xOffset == other[0] and self.yOffset == other[1] and self.zOffset == other[2]
314 else:
315 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
316 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
317 raise ex
319 def __ne__(self, other) -> bool:
320 """
321 Compare two 3D-offsets for inequality.
323 :param other: Parameter to compare against.
324 :returns: ``True``, if both 3D-offsets are unequal.
325 :raises TypeError: If parameter ``other`` is not of type :class:`Offset3D` or :class:`tuple`.
326 """
327 return not self.__eq__(other)
329 def __neg__(self) -> "Offset3D[Coordinate]":
330 """
331 Negate all components of this 3D-offset and create a new 3D-offset.
333 :returns: 3D-offset with negated offset components.
334 """
335 return self.__class__(
336 -self.xOffset,
337 -self.yOffset,
338 -self.zOffset
339 )
341 def __add__(self, other: Any) -> "Offset3D[Coordinate]":
342 """
343 Adds a 3D-offset to this 3D-offset and creates a new 3D-offset.
345 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`.
346 :returns: A new 3D-offset extended by the 3D-offset.
347 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
348 """
349 if isinstance(other, Offset3D):
350 return self.__class__(
351 self.xOffset + other.xOffset,
352 self.yOffset + other.yOffset,
353 self.zOffset + other.zOffset
354 )
355 elif isinstance(other, tuple):
356 return self.__class__(
357 self.xOffset + other[0],
358 self.yOffset + other[1],
359 self.zOffset + other[2]
360 )
361 else:
362 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
363 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
364 raise ex
366 def __iadd__(self, other: Any) -> "Offset3D[Coordinate]": # TODO: Python 3.11: -> Self:
367 """
368 Adds a 3D-offset to this 3D-offset (inplace).
370 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`.
371 :returns: This 3D-point.
372 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
373 """
374 if isinstance(other, Offset3D):
375 self.xOffset += other.xOffset
376 self.yOffset += other.yOffset
377 self.zOffset += other.zOffset
378 elif isinstance(other, tuple):
379 self.xOffset += other[0]
380 self.yOffset += other[1]
381 self.zOffset += other[2]
382 else:
383 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
384 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
385 raise ex
387 return self
389 def __sub__(self, other: Any) -> "Offset3D[Coordinate]":
390 """
391 Subtracts a 3D-offset from this 3D-offset and creates a new 3D-offset.
393 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`.
394 :returns: A new 3D-offset reduced by the 3D-offset.
395 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
396 """
397 if isinstance(other, Offset3D):
398 return self.__class__(
399 self.xOffset - other.xOffset,
400 self.yOffset - other.yOffset,
401 self.zOffset - other.zOffset
402 )
403 elif isinstance(other, tuple):
404 return self.__class__(
405 self.xOffset - other[0],
406 self.yOffset - other[1],
407 self.zOffset - other[2]
408 )
409 else:
410 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
411 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
412 raise ex
414 def __isub__(self, other: Any) -> "Offset3D[Coordinate]": # TODO: Python 3.11: -> Self:
415 """
416 Subtracts a 3D-offset from this 3D-offset (inplace).
418 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`.
419 :returns: This 3D-point.
420 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
421 """
422 if isinstance(other, Offset3D):
423 self.xOffset -= other.xOffset
424 self.yOffset -= other.yOffset
425 self.zOffset -= other.zOffset
426 elif isinstance(other, tuple):
427 self.xOffset -= other[0]
428 self.yOffset -= other[1]
429 self.zOffset -= other[2]
430 else:
431 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
432 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
433 raise ex
435 return self
437 def __repr__(self) -> str:
438 """
439 Returns the 3D offset's string representation.
441 :returns: The string representation of the 3D offset.
442 """
443 return f"Offset3D({self.xOffset}, {self.yOffset}, {self.zOffset})"
445 def __str__(self) -> str:
446 """
447 Returns the 3D offset's string equivalent.
449 :returns: The string equivalent of the 3D offset.
450 """
451 return f"({self.xOffset}, {self.yOffset}, {self.zOffset})"
454@export
455class Size3D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
456 """An implementation of a 3D cartesian size."""
458 width: Coordinate #: width in x-direction.
459 height: Coordinate #: height in y-direction.
460 depth: Coordinate #: depth in z-direction.
462 def __init__(self, width: Coordinate, height: Coordinate, depth: Coordinate) -> None:
463 """
464 Initializes a 2-dimensional size.
466 :param width: width in x-direction.
467 :param height: height in y-direction.
468 :param depth: depth in z-direction.
469 :raises TypeError: If width/height/depth is not of type integer or float.
470 """
471 if not isinstance(width, (int, float)):
472 ex = TypeError(f"Parameter 'width' is not of type integer or float.")
473 ex.add_note(f"Got type '{getFullyQualifiedName(width)}'.")
474 raise ex
475 if not isinstance(height, (int, float)):
476 ex = TypeError(f"Parameter 'height' is not of type integer or float.")
477 ex.add_note(f"Got type '{getFullyQualifiedName(height)}'.")
478 raise ex
479 if not isinstance(depth, (int, float)):
480 ex = TypeError(f"Parameter 'depth' is not of type integer or float.")
481 ex.add_note(f"Got type '{getFullyQualifiedName(depth)}'.")
482 raise ex
484 self.width = width
485 self.height = height
486 self.depth = depth
488 def Copy(self) -> "Size3D[Coordinate]": # TODO: Python 3.11: -> Self:
489 """
490 Create a new 3D-size as a copy of this 3D-size.
492 :returns: Copy of this 3D-size.
493 """
494 return self.__class__(self.width, self.height, self.depth)
496 def ToTuple(self) -> Tuple[Coordinate, Coordinate, Coordinate]:
497 """
498 Convert this 3D-size to a simple 3-element tuple.
500 :return: ``(width, height, depth)`` tuple.
501 """
502 return self.width, self.height, self.depth
504 def __repr__(self) -> str:
505 """
506 Returns the 3D size's string representation.
508 :returns: The string representation of the 3D size.
509 """
510 return f"Size3D({self.width}, {self.height}, {self.depth})"
512 def __str__(self) -> str:
513 """
514 Returns the 3D size's string equivalent.
516 :returns: The string equivalent of the 3D size.
517 """
518 return f"({self.width}, {self.height}, {self.depth})"
521@export
522class Segment3D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
523 """An implementation of a 3D cartesian segment."""
525 start: Point3D[Coordinate] #: Start point of a segment.
526 end: Point3D[Coordinate] #: End point of a segment.
528 def __init__(self, start: Point3D[Coordinate], end: Point3D[Coordinate], copyPoints: bool = True) -> None:
529 """
530 Initializes a 3-dimensional segment.
532 :param start: Start point of the segment.
533 :param end: End point of the segment.
534 :raises TypeError: If start/end is not of type Point3D.
535 """
536 if not isinstance(start, Point3D): 536 ↛ 537line 536 didn't jump to line 537 because the condition on line 536 was never true
537 ex = TypeError(f"Parameter 'start' is not of type Point3D.")
538 ex.add_note(f"Got type '{getFullyQualifiedName(start)}'.")
539 raise ex
540 if not isinstance(end, Point3D): 540 ↛ 541line 540 didn't jump to line 541 because the condition on line 540 was never true
541 ex = TypeError(f"Parameter 'end' is not of type Point3D.")
542 ex.add_note(f"Got type '{getFullyQualifiedName(end)}'.")
543 raise ex
545 self.start = start.Copy() if copyPoints else start
546 self.end = end.Copy() if copyPoints else end
549@export
550class LineSegment3D(Segment3D[Coordinate], Generic[Coordinate]):
551 """An implementation of a 3D cartesian line segment."""
553 @readonly
554 def Length(self) -> float:
555 """
556 Read-only property to return the Euclidean distance between start and end point.
558 :return: Euclidean distance between start and end point
559 """
560 return sqrt((self.end.x - self.start.x) ** 2 + (self.end.y - self.start.y) ** 2 + (self.end.z - self.start.z) ** 2)
562 def AngleTo(self, other: "LineSegment3D[Coordinate]") -> float:
563 vectorA = self.ToOffset()
564 vectorB = other.ToOffset()
565 scalarProductAB = vectorA.xOffset * vectorB.xOffset + vectorA.yOffset * vectorB.yOffset + vectorA.zOffset * vectorB.zOffset
567 return acos(scalarProductAB / (abs(self.Length) * abs(other.Length)))
569 def ToOffset(self) -> Offset3D[Coordinate]:
570 """
571 Convert this 3D line segment to a 3D-offset.
573 :return: 3D-offset as :class:`Offset3D`
574 """
575 return self.end - self.start
577 def ToTuple(self) -> Tuple[Tuple[Coordinate, Coordinate, Coordinate], Tuple[Coordinate, Coordinate, Coordinate]]:
578 """
579 Convert this 3D line segment to a simple 2-element tuple of 3D-point tuples.
581 :return: ``((x1, y1, z1), (x2, y2, z2))`` tuple.
582 """
583 return self.start.ToTuple(), self.end.ToTuple()
585 def __repr__(self) -> str:
586 """
587 Returns the 3D line segment's string representation.
589 :returns: The string representation of the 3D line segment.
590 """
591 return f"LineSegment3D({self.start}, {self.end})"
593 def __str__(self) -> str:
594 """
595 Returns the 3D line segment's string equivalent.
597 :returns: The string equivalent of the 3D line segment.
598 """
599 return f"({self.start} → {self.end})"