Coverage for pyTooling/Cartesian3D/__init__.py: 94%
231 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-31 22:23 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-31 22:23 +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."""
32from sys import version_info
34from math import sqrt, acos
35from typing import Union, Generic, Any, Tuple
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
42 from pyTooling.Cartesian2D import Coordinate
43except (ImportError, ModuleNotFoundError): # pragma: no cover
44 print("[pyTooling.Cartesian2D] Could not import from 'pyTooling.*'!")
46 try:
47 from Decorators import readonly, export
48 from Exceptions import ToolingException
49 from MetaClasses import ExtendedType
50 from Common import getFullyQualifiedName
51 from Cartesian2D import Coordinate
52 except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover
53 print("[pyTooling.Cartesian2D] Could not import directly!")
54 raise ex
57@export
58class Point3D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
59 """An implementation of a 3D cartesian point."""
61 x: Coordinate #: The x-direction coordinate.
62 y: Coordinate #: The y-direction coordinate.
63 z: Coordinate #: The z-direction coordinate.
65 def __init__(self, x: Coordinate, y: Coordinate, z: Coordinate) -> None:
66 """
67 Initializes a 3-dimensional point.
69 :param x: X-coordinate.
70 :param y: Y-coordinate.
71 :param z: Z-coordinate.
72 :raises TypeError: If x/y/z-coordinate is not of type integer or float.
73 """
74 if not isinstance(x, (int, float)):
75 ex = TypeError(f"Parameter 'x' is not of type integer or float.")
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 ex.add_note(f"Got type '{getFullyQualifiedName(y)}'.")
81 raise ex
82 if not isinstance(z, (int, float)):
83 ex = TypeError(f"Parameter 'z' is not of type integer or float.")
84 ex.add_note(f"Got type '{getFullyQualifiedName(z)}'.")
85 raise ex
87 self.x = x
88 self.y = y
89 self.z = z
91 def Copy(self) -> "Point3D[Coordinate]": # TODO: Python 3.11: -> Self:
92 """
93 Create a new 3D-point as a copy of this 3D point.
95 :return: Copy of this 3D-point.
97 .. seealso::
99 :meth:`+ operator <__add__>`
100 Create a new 3D-point moved by a positive 3D-offset.
101 :meth:`- operator <__sub__>`
102 Create a new 3D-point moved by a negative 3D-offset.
103 """
104 return self.__class__(self.x, self.y, self.z)
106 def ToTuple(self) -> Tuple[Coordinate, Coordinate, Coordinate]:
107 """
108 Convert this 3D-Point to a simple 3-element tuple.
110 :return: ``(x, y, z)`` tuple.
111 """
112 return self.x, self.y, self.z
114 def __add__(self, other: Any) -> "Point3D[Coordinate]":
115 """
116 Adds a 3D-offset to this 3D-point and creates a new 3D-point.
118 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`.
119 :return: A new 3D-point shifted by the 3D-offset.
120 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
121 """
122 if isinstance(other, Offset3D):
123 return self.__class__(
124 self.x + other.xOffset,
125 self.y + other.yOffset,
126 self.z + other.zOffset
127 )
128 elif isinstance(other, tuple):
129 return self.__class__(
130 self.x + other[0],
131 self.y + other[1],
132 self.z + other[2]
133 )
134 else:
135 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
136 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
137 raise ex
139 def __iadd__(self, other: Any) -> "Point3D[Coordinate]": # TODO: Python 3.11: -> Self:
140 """
141 Adds a 3D-offset to this 3D-point (inplace).
143 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`.
144 :return: This 3D-point.
145 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
146 """
147 if isinstance(other, Offset3D):
148 self.x += other.xOffset
149 self.y += other.yOffset
150 self.z += other.zOffset
151 elif isinstance(other, tuple):
152 self.x += other[0]
153 self.y += other[1]
154 self.z += other[2]
155 else:
156 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
157 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
158 raise ex
160 return self
162 def __sub__(self, other: Any) -> Union["Offset3D[Coordinate]", "Point3D[Coordinate]"]:
163 """
164 Subtract two 3D-Points from each other and create a new 3D-offset.
166 :param other: A 3D-point as :class:`Point3D`.
167 :return: A new 3D-offset representing the distance between these two points.
168 :raises TypeError: If parameter 'other' is not a :class:`Point3D`.
169 """
170 if isinstance(other, Point3D):
171 return Offset3D(
172 self.x - other.x,
173 self.y - other.y,
174 self.z - other.z
175 )
176 else:
177 ex = TypeError(f"Parameter 'other' is not of type Point3D.")
178 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
179 raise ex
181 def __isub__(self, other: Any) -> "Point3D[Coordinate]": # TODO: Python 3.11: -> Self:
182 """
183 Subtracts a 3D-offset to this 3D-point (inplace).
185 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`.
186 :return: This 3D-point.
187 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
188 """
189 if isinstance(other, Offset3D):
190 self.x -= other.xOffset
191 self.y -= other.yOffset
192 self.z -= other.zOffset
193 elif isinstance(other, tuple):
194 self.x -= other[0]
195 self.y -= other[1]
196 self.z -= other[2]
197 else:
198 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
199 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
200 raise ex
202 return self
204 def __repr__(self) -> str:
205 """
206 Returns the 3D point's string representation.
208 :returns: The string representation of the 3D point.
209 """
210 return f"Point3D({self.x}, {self.y}, {self.z})"
212 def __str__(self) -> str:
213 """
214 Returns the 3D point's string equivalent.
216 :returns: The string equivalent of the 3D point.
217 """
218 return f"({self.x}, {self.y}, {self.z})"
221@export
222class Origin3D(Point3D[Coordinate], Generic[Coordinate]):
223 """An implementation of a 3D cartesian origin."""
225 def __init__(self) -> None:
226 """
227 Initializes a 3-dimensional origin.
228 """
229 super().__init__(0, 0, 0)
231 def Copy(self) -> "Origin3D[Coordinate]": # TODO: Python 3.11: -> Self:
232 """
233 :raises RuntimeError: Because an origin can't be copied.
234 """
235 raise RuntimeError(f"An origin can't be copied.")
237 def __repr__(self) -> str:
238 """
239 Returns the 3D origin's string representation.
241 :returns: The string representation of the 3D origin.
242 """
243 return f"Origin3D({self.x}, {self.y}, {self.z})"
246@export
247class Offset3D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
248 """An implementation of a 3D cartesian offset."""
250 xOffset: Coordinate #: The x-direction offset
251 yOffset: Coordinate #: The y-direction offset
252 zOffset: Coordinate #: The z-direction offset
254 def __init__(self, xOffset: Coordinate, yOffset: Coordinate, zOffset: Coordinate) -> None:
255 """
256 Initializes a 3-dimensional offset.
258 :param xOffset: x-direction offset.
259 :param yOffset: y-direction offset.
260 :param zOffset: z-direction offset.
261 :raises TypeError: If x/y/z-offset is not of type integer or float.
262 """
263 if not isinstance(xOffset, (int, float)):
264 ex = TypeError(f"Parameter 'xOffset' is not of type integer or float.")
265 ex.add_note(f"Got type '{getFullyQualifiedName(xOffset)}'.")
266 raise ex
267 if not isinstance(yOffset, (int, float)):
268 ex = TypeError(f"Parameter 'yOffset' is not of type integer or float.")
269 ex.add_note(f"Got type '{getFullyQualifiedName(yOffset)}'.")
270 raise ex
271 if not isinstance(zOffset, (int, float)):
272 ex = TypeError(f"Parameter 'zOffset' is not of type integer or float.")
273 ex.add_note(f"Got type '{getFullyQualifiedName(zOffset)}'.")
274 raise ex
276 self.xOffset = xOffset
277 self.yOffset = yOffset
278 self.zOffset = zOffset
280 def Copy(self) -> "Offset3D[Coordinate]": # TODO: Python 3.11: -> Self:
281 """
282 Create a new 3D-offset as a copy of this 3D-offset.
284 :returns: Copy of this 3D-offset.
286 .. seealso::
288 :meth:`+ operator <__add__>`
289 Create a new 3D-offset moved by a positive 3D-offset.
290 :meth:`- operator <__sub__>`
291 Create a new 3D-offset moved by a negative 3D-offset.
292 """
293 return self.__class__(self.xOffset, self.yOffset, self.zOffset)
295 def ToTuple(self) -> Tuple[Coordinate, Coordinate, Coordinate]:
296 """
297 Convert this 3D-offset to a simple 3-element tuple.
299 :returns: ``(x, y, z)`` tuple.
300 """
301 return self.xOffset, self.yOffset, self.zOffset
303 def __eq__(self, other) -> bool:
304 """
305 Compare two 3D-offsets for equality.
307 :param other: Parameter to compare against.
308 :returns: ``True``, if both 3D-offsets are equal.
309 :raises TypeError: If parameter ``other`` is not of type :class:`Offset3D` or :class:`tuple`.
310 """
311 if isinstance(other, Offset3D):
312 return self.xOffset == other.xOffset and self.yOffset == other.yOffset and self.zOffset == other.zOffset
313 elif isinstance(other, tuple):
314 return self.xOffset == other[0] and self.yOffset == other[1] and self.zOffset == other[2]
315 else:
316 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
317 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
318 raise ex
320 def __ne__(self, other) -> bool:
321 """
322 Compare two 3D-offsets for inequality.
324 :param other: Parameter to compare against.
325 :returns: ``True``, if both 3D-offsets are unequal.
326 :raises TypeError: If parameter ``other`` is not of type :class:`Offset3D` or :class:`tuple`.
327 """
328 return not self.__eq__(other)
330 def __neg__(self) -> "Offset3D[Coordinate]":
331 """
332 Negate all components of this 3D-offset and create a new 3D-offset.
334 :returns: 3D-offset with negated offset components.
335 """
336 return self.__class__(
337 -self.xOffset,
338 -self.yOffset,
339 -self.zOffset
340 )
342 def __add__(self, other: Any) -> "Offset3D[Coordinate]":
343 """
344 Adds a 3D-offset to this 3D-offset and creates a new 3D-offset.
346 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`.
347 :returns: A new 3D-offset extended by the 3D-offset.
348 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
349 """
350 if isinstance(other, Offset3D):
351 return self.__class__(
352 self.xOffset + other.xOffset,
353 self.yOffset + other.yOffset,
354 self.zOffset + other.zOffset
355 )
356 elif isinstance(other, tuple):
357 return self.__class__(
358 self.xOffset + other[0],
359 self.yOffset + other[1],
360 self.zOffset + other[2]
361 )
362 else:
363 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
364 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
365 raise ex
367 def __iadd__(self, other: Any) -> "Offset3D[Coordinate]": # TODO: Python 3.11: -> Self:
368 """
369 Adds a 3D-offset to this 3D-offset (inplace).
371 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`.
372 :returns: This 3D-point.
373 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
374 """
375 if isinstance(other, Offset3D):
376 self.xOffset += other.xOffset
377 self.yOffset += other.yOffset
378 self.zOffset += other.zOffset
379 elif isinstance(other, tuple):
380 self.xOffset += other[0]
381 self.yOffset += other[1]
382 self.zOffset += other[2]
383 else:
384 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
385 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
386 raise ex
388 return self
390 def __sub__(self, other: Any) -> "Offset3D[Coordinate]":
391 """
392 Subtracts a 3D-offset from this 3D-offset and creates a new 3D-offset.
394 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`.
395 :returns: A new 3D-offset reduced by the 3D-offset.
396 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
397 """
398 if isinstance(other, Offset3D):
399 return self.__class__(
400 self.xOffset - other.xOffset,
401 self.yOffset - other.yOffset,
402 self.zOffset - other.zOffset
403 )
404 elif isinstance(other, tuple):
405 return self.__class__(
406 self.xOffset - other[0],
407 self.yOffset - other[1],
408 self.zOffset - other[2]
409 )
410 else:
411 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
412 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
413 raise ex
415 def __isub__(self, other: Any) -> "Offset3D[Coordinate]": # TODO: Python 3.11: -> Self:
416 """
417 Subtracts a 3D-offset from this 3D-offset (inplace).
419 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`.
420 :returns: This 3D-point.
421 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
422 """
423 if isinstance(other, Offset3D):
424 self.xOffset -= other.xOffset
425 self.yOffset -= other.yOffset
426 self.zOffset -= other.zOffset
427 elif isinstance(other, tuple):
428 self.xOffset -= other[0]
429 self.yOffset -= other[1]
430 self.zOffset -= other[2]
431 else:
432 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
433 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
434 raise ex
436 return self
438 def __repr__(self) -> str:
439 """
440 Returns the 3D offset's string representation.
442 :returns: The string representation of the 3D offset.
443 """
444 return f"Offset3D({self.xOffset}, {self.yOffset}, {self.zOffset})"
446 def __str__(self) -> str:
447 """
448 Returns the 3D offset's string equivalent.
450 :returns: The string equivalent of the 3D offset.
451 """
452 return f"({self.xOffset}, {self.yOffset}, {self.zOffset})"
455@export
456class Size3D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
457 """An implementation of a 3D cartesian size."""
459 width: Coordinate #: width in x-direction.
460 height: Coordinate #: height in y-direction.
461 depth: Coordinate #: depth in z-direction.
463 def __init__(self, width: Coordinate, height: Coordinate, depth: Coordinate) -> None:
464 """
465 Initializes a 2-dimensional size.
467 :param width: width in x-direction.
468 :param height: height in y-direction.
469 :param depth: depth in z-direction.
470 :raises TypeError: If width/height/depth is not of type integer or float.
471 """
472 if not isinstance(width, (int, float)):
473 ex = TypeError(f"Parameter 'width' is not of type integer or float.")
474 ex.add_note(f"Got type '{getFullyQualifiedName(width)}'.")
475 raise ex
476 if not isinstance(height, (int, float)):
477 ex = TypeError(f"Parameter 'height' is not of type integer or float.")
478 ex.add_note(f"Got type '{getFullyQualifiedName(height)}'.")
479 raise ex
480 if not isinstance(depth, (int, float)):
481 ex = TypeError(f"Parameter 'depth' is not of type integer or float.")
482 ex.add_note(f"Got type '{getFullyQualifiedName(depth)}'.")
483 raise ex
485 self.width = width
486 self.height = height
487 self.depth = depth
489 def Copy(self) -> "Size3D[Coordinate]": # TODO: Python 3.11: -> Self:
490 """
491 Create a new 3D-size as a copy of this 3D-size.
493 :returns: Copy of this 3D-size.
494 """
495 return self.__class__(self.width, self.height, self.depth)
497 def ToTuple(self) -> Tuple[Coordinate, Coordinate, Coordinate]:
498 """
499 Convert this 3D-size to a simple 3-element tuple.
501 :return: ``(width, height, depth)`` tuple.
502 """
503 return self.width, self.height, self.depth
505 def __repr__(self) -> str:
506 """
507 Returns the 3D size's string representation.
509 :returns: The string representation of the 3D size.
510 """
511 return f"Size3D({self.width}, {self.height}, {self.depth})"
513 def __str__(self) -> str:
514 """
515 Returns the 3D size's string equivalent.
517 :returns: The string equivalent of the 3D size.
518 """
519 return f"({self.width}, {self.height}, {self.depth})"
522@export
523class Segment3D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
524 """An implementation of a 3D cartesian segment."""
526 start: Point3D[Coordinate] #: Start point of a segment.
527 end: Point3D[Coordinate] #: End point of a segment.
529 def __init__(self, start: Point3D[Coordinate], end: Point3D[Coordinate], copyPoints: bool = True) -> None:
530 """
531 Initializes a 3-dimensional segment.
533 :param start: Start point of the segment.
534 :param end: End point of the segment.
535 :raises TypeError: If start/end is not of type Point3D.
536 """
537 if not isinstance(start, Point3D): 537 ↛ 538line 537 didn't jump to line 538 because the condition on line 537 was never true
538 ex = TypeError(f"Parameter 'start' is not of type Point3D.")
539 ex.add_note(f"Got type '{getFullyQualifiedName(start)}'.")
540 raise ex
541 if not isinstance(end, Point3D): 541 ↛ 542line 541 didn't jump to line 542 because the condition on line 541 was never true
542 ex = TypeError(f"Parameter 'end' is not of type Point3D.")
543 ex.add_note(f"Got type '{getFullyQualifiedName(end)}'.")
544 raise ex
546 self.start = start.Copy() if copyPoints else start
547 self.end = end.Copy() if copyPoints else end
550@export
551class LineSegment3D(Segment3D[Coordinate], Generic[Coordinate]):
552 """An implementation of a 3D cartesian line segment."""
554 @readonly
555 def Length(self) -> float:
556 """
557 Read-only property to return the Euclidean distance between start and end point.
559 :return: Euclidean distance between start and end point
560 """
561 return sqrt((self.end.x - self.start.x) ** 2 + (self.end.y - self.start.y) ** 2 + (self.end.z - self.start.z) ** 2)
563 def AngleTo(self, other: "LineSegment3D[Coordinate]") -> float:
564 vectorA = self.ToOffset()
565 vectorB = other.ToOffset()
566 scalarProductAB = vectorA.xOffset * vectorB.xOffset + vectorA.yOffset * vectorB.yOffset + vectorA.zOffset * vectorB.zOffset
568 return acos(scalarProductAB / (abs(self.Length) * abs(other.Length)))
570 def ToOffset(self) -> Offset3D[Coordinate]:
571 """
572 Convert this 3D line segment to a 3D-offset.
574 :return: 3D-offset as :class:`Offset3D`
575 """
576 return self.end - self.start
578 def ToTuple(self) -> Tuple[Tuple[Coordinate, Coordinate, Coordinate], Tuple[Coordinate, Coordinate, Coordinate]]:
579 """
580 Convert this 3D line segment to a simple 2-element tuple of 3D-point tuples.
582 :return: ``((x1, y1, z1), (x2, y2, z2))`` tuple.
583 """
584 return self.start.ToTuple(), self.end.ToTuple()
586 def __repr__(self) -> str:
587 """
588 Returns the 3D line segment's string representation.
590 :returns: The string representation of the 3D line segment.
591 """
592 return f"LineSegment3D({self.start}, {self.end})"
594 def __str__(self) -> str:
595 """
596 Returns the 3D line segment's string equivalent.
598 :returns: The string equivalent of the 3D line segment.
599 """
600 return f"({self.start} → {self.end})"