Coverage for pyTooling/Cartesian3D/__init__.py: 94%
211 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-18 22:20 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-18 22:20 +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 if version_info >= (3, 11): # pragma: no cover
77 ex.add_note(f"Got type '{getFullyQualifiedName(x)}'.")
78 raise ex
79 if not isinstance(y, (int, float)):
80 ex = TypeError(f"Parameter 'y' is not of type integer or float.")
81 if version_info >= (3, 11): # pragma: no cover
82 ex.add_note(f"Got type '{getFullyQualifiedName(y)}'.")
83 raise ex
84 if not isinstance(z, (int, float)):
85 ex = TypeError(f"Parameter 'z' is not of type integer or float.")
86 if version_info >= (3, 11): # pragma: no cover
87 ex.add_note(f"Got type '{getFullyQualifiedName(z)}'.")
88 raise ex
90 self.x = x
91 self.y = y
92 self.z = z
94 def Copy(self) -> "Point3D[Coordinate]": # TODO: Python 3.11: -> Self:
95 """
96 Create a new 3D-point as a copy of this 3D point.
98 :return: Copy of this 3D-point.
100 .. seealso::
102 :meth:`+ operator <__add__>`
103 Create a new 3D-point moved by a positive 3D-offset.
104 :meth:`- operator <__sub__>`
105 Create a new 3D-point moved by a negative 3D-offset.
106 """
107 return self.__class__(self.x, self.y, self.z)
109 def ToTuple(self) -> Tuple[Coordinate, Coordinate, Coordinate]:
110 """
111 Convert this 3D-Point to a simple 3-element tuple.
113 :return: ``(x, y, z)`` tuple.
114 """
115 return self.x, self.y, self.z
117 def __add__(self, other: Any) -> "Point3D[Coordinate]":
118 """
119 Adds a 3D-offset to this 3D-point and creates a new 3D-point.
121 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`.
122 :return: A new 3D-point shifted by the 3D-offset.
123 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
124 """
125 if isinstance(other, Offset3D):
126 return self.__class__(
127 self.x + other.xOffset,
128 self.y + other.yOffset,
129 self.z + other.zOffset
130 )
131 elif isinstance(other, tuple):
132 return self.__class__(
133 self.x + other[0],
134 self.y + other[1],
135 self.z + other[2]
136 )
137 else:
138 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
139 if version_info >= (3, 11): # pragma: no cover
140 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
141 raise ex
143 def __iadd__(self, other: Any) -> "Point3D[Coordinate]": # TODO: Python 3.11: -> Self:
144 """
145 Adds a 3D-offset to this 3D-point (inplace).
147 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`.
148 :return: This 3D-point.
149 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
150 """
151 if isinstance(other, Offset3D):
152 self.x += other.xOffset
153 self.y += other.yOffset
154 self.z += other.zOffset
155 elif isinstance(other, tuple):
156 self.x += other[0]
157 self.y += other[1]
158 self.z += other[2]
159 else:
160 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
161 if version_info >= (3, 11): # pragma: no cover
162 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
163 raise ex
165 return self
167 def __sub__(self, other: Any) -> Union["Offset3D[Coordinate]", "Point3D[Coordinate]"]:
168 """
169 Subtract two 3D-Points from each other and create a new 3D-offset.
171 :param other: A 3D-point as :class:`Point3D`.
172 :return: A new 3D-offset representing the distance between these two points.
173 :raises TypeError: If parameter 'other' is not a :class:`Point3D`.
174 """
175 if isinstance(other, Point3D):
176 return Offset3D(
177 self.x - other.x,
178 self.y - other.y,
179 self.z - other.z
180 )
181 else:
182 ex = TypeError(f"Parameter 'other' is not of type Point3D.")
183 if version_info >= (3, 11): # pragma: no cover
184 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
185 raise ex
187 def __isub__(self, other: Any) -> "Point3D[Coordinate]": # TODO: Python 3.11: -> Self:
188 """
189 Subtracts a 3D-offset to this 3D-point (inplace).
191 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`.
192 :return: This 3D-point.
193 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
194 """
195 if isinstance(other, Offset3D):
196 self.x -= other.xOffset
197 self.y -= other.yOffset
198 self.z -= other.zOffset
199 elif isinstance(other, tuple):
200 self.x -= other[0]
201 self.y -= other[1]
202 self.z -= other[2]
203 else:
204 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
205 if version_info >= (3, 11): # pragma: no cover
206 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
207 raise ex
209 return self
211 def __repr__(self) -> str:
212 return f"Point3D({self.x}, {self.y}, {self.z})"
214 def __str__(self) -> str:
215 return f"({self.x}, {self.y}, {self.z})"
218@export
219class Origin3D(Point3D[Coordinate], Generic[Coordinate]):
220 """An implementation of a 3D cartesian origin."""
222 def __init__(self) -> None:
223 """
224 Initializes a 3-dimensional origin.
225 """
226 super().__init__(0, 0, 0)
228 def Copy(self) -> "Origin3D[Coordinate]": # TODO: Python 3.11: -> Self:
229 """
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 return f"Origin3D({self.x}, {self.y}, {self.z})"
238@export
239class Offset3D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
240 """An implementation of a 3D cartesian offset."""
242 xOffset: Coordinate #: The x-direction offset
243 yOffset: Coordinate #: The y-direction offset
244 zOffset: Coordinate #: The z-direction offset
246 def __init__(self, xOffset: Coordinate, yOffset: Coordinate, zOffset: Coordinate) -> None:
247 """
248 Initializes a 3-dimensional offset.
250 :param xOffset: x-direction offset.
251 :param yOffset: y-direction offset.
252 :param zOffset: z-direction offset.
253 :raises TypeError: If x/y/z-offset is not of type integer or float.
254 """
255 if not isinstance(xOffset, (int, float)):
256 ex = TypeError(f"Parameter 'xOffset' is not of type integer or float.")
257 if version_info >= (3, 11): # pragma: no cover
258 ex.add_note(f"Got type '{getFullyQualifiedName(xOffset)}'.")
259 raise ex
260 if not isinstance(yOffset, (int, float)):
261 ex = TypeError(f"Parameter 'yOffset' is not of type integer or float.")
262 if version_info >= (3, 11): # pragma: no cover
263 ex.add_note(f"Got type '{getFullyQualifiedName(yOffset)}'.")
264 raise ex
265 if not isinstance(zOffset, (int, float)):
266 ex = TypeError(f"Parameter 'zOffset' is not of type integer or float.")
267 if version_info >= (3, 11): # pragma: no cover
268 ex.add_note(f"Got type '{getFullyQualifiedName(zOffset)}'.")
269 raise ex
271 self.xOffset = xOffset
272 self.yOffset = yOffset
273 self.zOffset = zOffset
275 def Copy(self) -> "Offset3D[Coordinate]": # TODO: Python 3.11: -> Self:
276 """
277 Create a new 3D-offset as a copy of this 3D-offset.
279 :returns: Copy of this 3D-offset.
281 .. seealso::
283 :meth:`+ operator <__add__>`
284 Create a new 3D-offset moved by a positive 3D-offset.
285 :meth:`- operator <__sub__>`
286 Create a new 3D-offset moved by a negative 3D-offset.
287 """
288 return self.__class__(self.xOffset, self.yOffset, self.zOffset)
290 def ToTuple(self) -> Tuple[Coordinate, Coordinate, Coordinate]:
291 """
292 Convert this 3D-offset to a simple 3-element tuple.
294 :returns: ``(x, y, z)`` tuple.
295 """
296 return self.xOffset, self.yOffset, self.zOffset
298 def __eq__(self, other) -> bool:
299 """
300 Compare two 3D-offsets for equality.
302 :param other: Parameter to compare against.
303 :returns: ``True``, if both 3D-offsets are equal.
304 :raises TypeError: If parameter ``other`` is not of type :class:`Offset3D` or :class:`tuple`.
305 """
306 if isinstance(other, Offset3D):
307 return self.xOffset == other.xOffset and self.yOffset == other.yOffset and self.zOffset == other.zOffset
308 elif isinstance(other, tuple):
309 return self.xOffset == other[0] and self.yOffset == other[1] and self.zOffset == other[2]
310 else:
311 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
312 if version_info >= (3, 11): # pragma: no cover
313 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
314 raise ex
316 def __ne__(self, other) -> bool:
317 """
318 Compare two 3D-offsets for inequality.
320 :param other: Parameter to compare against.
321 :returns: ``True``, if both 3D-offsets are unequal.
322 :raises TypeError: If parameter ``other`` is not of type :class:`Offset3D` or :class:`tuple`.
323 """
324 return not self.__eq__(other)
326 def __neg__(self) -> "Offset3D[Coordinate]":
327 """
328 Negate all components of this 3D-offset and create a new 3D-offset.
330 :returns: 3D-offset with negated offset components.
331 """
332 return self.__class__(
333 -self.xOffset,
334 -self.yOffset,
335 -self.zOffset
336 )
338 def __add__(self, other: Any) -> "Offset3D[Coordinate]":
339 """
340 Adds a 3D-offset to this 3D-offset and creates a new 3D-offset.
342 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`.
343 :returns: A new 3D-offset extended by the 3D-offset.
344 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
345 """
346 if isinstance(other, Offset3D):
347 return self.__class__(
348 self.xOffset + other.xOffset,
349 self.yOffset + other.yOffset,
350 self.zOffset + other.zOffset
351 )
352 elif isinstance(other, tuple):
353 return self.__class__(
354 self.xOffset + other[0],
355 self.yOffset + other[1],
356 self.zOffset + other[2]
357 )
358 else:
359 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
360 if version_info >= (3, 11): # pragma: no cover
361 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
362 raise ex
364 def __iadd__(self, other: Any) -> "Offset3D[Coordinate]": # TODO: Python 3.11: -> Self:
365 """
366 Adds a 3D-offset to this 3D-offset (inplace).
368 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`.
369 :returns: This 3D-point.
370 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`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 if version_info >= (3, 11): # pragma: no cover
383 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
384 raise ex
386 return self
388 def __sub__(self, other: Any) -> "Offset3D[Coordinate]":
389 """
390 Subtracts a 3D-offset from this 3D-offset and creates a new 3D-offset.
392 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`.
393 :returns: A new 3D-offset reduced by the 3D-offset.
394 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
395 """
396 if isinstance(other, Offset3D):
397 return self.__class__(
398 self.xOffset - other.xOffset,
399 self.yOffset - other.yOffset,
400 self.zOffset - other.zOffset
401 )
402 elif isinstance(other, tuple):
403 return self.__class__(
404 self.xOffset - other[0],
405 self.yOffset - other[1],
406 self.zOffset - other[2]
407 )
408 else:
409 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
410 if version_info >= (3, 11): # pragma: no cover
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 if version_info >= (3, 11): # pragma: no cover
433 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
434 raise ex
436 return self
438 def __repr__(self) -> str:
439 return f"Offset3D({self.xOffset}, {self.yOffset}, {self.zOffset})"
441 def __str__(self) -> str:
442 return f"({self.xOffset}, {self.yOffset}, {self.zOffset})"
445@export
446class Size3D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
447 """An implementation of a 3D cartesian size."""
449 width: Coordinate #: width in x-direction.
450 height: Coordinate #: height in y-direction.
451 depth: Coordinate #: depth in z-direction.
453 def __init__(self, width: Coordinate, height: Coordinate, depth: Coordinate) -> None:
454 """
455 Initializes a 2-dimensional size.
457 :param width: width in x-direction.
458 :param height: height in y-direction.
459 :param depth: depth in z-direction.
460 :raises TypeError: If width/height/depth is not of type integer or float.
461 """
462 if not isinstance(width, (int, float)):
463 ex = TypeError(f"Parameter 'width' is not of type integer or float.")
464 if version_info >= (3, 11): # pragma: no cover
465 ex.add_note(f"Got type '{getFullyQualifiedName(width)}'.")
466 raise ex
467 if not isinstance(height, (int, float)):
468 ex = TypeError(f"Parameter 'height' is not of type integer or float.")
469 if version_info >= (3, 11): # pragma: no cover
470 ex.add_note(f"Got type '{getFullyQualifiedName(height)}'.")
471 raise ex
472 if not isinstance(depth, (int, float)):
473 ex = TypeError(f"Parameter 'depth' is not of type integer or float.")
474 if version_info >= (3, 11): # pragma: no cover
475 ex.add_note(f"Got type '{getFullyQualifiedName(depth)}'.")
476 raise ex
478 self.width = width
479 self.height = height
480 self.depth = depth
482 def Copy(self) -> "Size3D[Coordinate]": # TODO: Python 3.11: -> Self:
483 """
484 Create a new 3D-size as a copy of this 3D-size.
486 :returns: Copy of this 3D-size.
487 """
488 return self.__class__(self.width, self.height, self.depth)
490 def ToTuple(self) -> Tuple[Coordinate, Coordinate, Coordinate]:
491 """
492 Convert this 3D-size to a simple 3-element tuple.
494 :return: ``(width, height, depth)`` tuple.
495 """
496 return self.width, self.height, self.depth
498 def __repr__(self) -> str:
499 return f"Size3D({self.width}, {self.height}, {self.depth})"
501 def __str__(self) -> str:
502 return f"({self.width}, {self.height}, {self.depth})"
505@export
506class Segment3D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
507 """An implementation of a 3D cartesian segment."""
509 start: Point3D[Coordinate] #: Start point of a segment.
510 end: Point3D[Coordinate] #: End point of a segment.
512 def __init__(self, start: Point3D[Coordinate], end: Point3D[Coordinate], copyPoints: bool = True) -> None:
513 """
514 Initializes a 3-dimensional segment.
516 :param start: Start point of the segment.
517 :param end: End point of the segment.
518 :raises TypeError: If start/end is not of type Point3D.
519 """
520 if not isinstance(start, Point3D): 520 ↛ 521line 520 didn't jump to line 521 because the condition on line 520 was never true
521 ex = TypeError(f"Parameter 'start' is not of type Point3D.")
522 if version_info >= (3, 11): # pragma: no cover
523 ex.add_note(f"Got type '{getFullyQualifiedName(start)}'.")
524 raise ex
525 if not isinstance(end, Point3D): 525 ↛ 526line 525 didn't jump to line 526 because the condition on line 525 was never true
526 ex = TypeError(f"Parameter 'end' is not of type Point3D.")
527 if version_info >= (3, 11): # pragma: no cover
528 ex.add_note(f"Got type '{getFullyQualifiedName(end)}'.")
529 raise ex
531 self.start = start.Copy() if copyPoints else start
532 self.end = end.Copy() if copyPoints else end
535@export
536class LineSegment3D(Segment3D[Coordinate], Generic[Coordinate]):
537 """An implementation of a 3D cartesian line segment."""
539 @readonly
540 def Length(self) -> float:
541 """
542 Read-only property to return the Euclidean distance between start and end point.
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)
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
553 return acos(scalarProductAB / (abs(self.Length) * abs(other.Length)))
555 def ToOffset(self) -> Offset3D[Coordinate]:
556 """
557 Convert this 3D line segment to a 3D-offset.
559 :return: 3D-offset as :class:`Offset3D`
560 """
561 return self.end - self.start
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.
567 :return: ``((x1, y1, z1), (x2, y2, z2))`` tuple.
568 """
569 return self.start.ToTuple(), self.end.ToTuple()
571 def __repr__(self) -> str:
572 return f"LineSegment3D({self.start}, {self.end})"
574 def __str__(self) -> str:
575 return f"({self.start} → {self.end})"