Coverage for pyTooling / Cartesian3D / __init__.py: 94%
228 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-13 22:36 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-13 22:36 +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"""An implementation of 3D cartesian data structures for Python."""
33from math import sqrt, acos
34from typing import Union, Generic, Any, Tuple
36from pyTooling.Decorators import readonly, export
37from pyTooling.MetaClasses import ExtendedType
38from pyTooling.Common import getFullyQualifiedName
39from pyTooling.Cartesian2D import Coordinate
42@export
43class Point3D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
44 """An implementation of a 3D cartesian point."""
46 x: Coordinate #: The x-direction coordinate.
47 y: Coordinate #: The y-direction coordinate.
48 z: Coordinate #: The z-direction coordinate.
50 def __init__(self, x: Coordinate, y: Coordinate, z: Coordinate) -> None:
51 """
52 Initializes a 3-dimensional point.
54 :param x: X-coordinate.
55 :param y: Y-coordinate.
56 :param z: Z-coordinate.
57 :raises TypeError: If x/y/z-coordinate is not of type integer or float.
58 """
59 if not isinstance(x, (int, float)):
60 ex = TypeError(f"Parameter 'x' is not of type integer or float.")
61 ex.add_note(f"Got type '{getFullyQualifiedName(x)}'.")
62 raise ex
63 if not isinstance(y, (int, float)):
64 ex = TypeError(f"Parameter 'y' is not of type integer or float.")
65 ex.add_note(f"Got type '{getFullyQualifiedName(y)}'.")
66 raise ex
67 if not isinstance(z, (int, float)):
68 ex = TypeError(f"Parameter 'z' is not of type integer or float.")
69 ex.add_note(f"Got type '{getFullyQualifiedName(z)}'.")
70 raise ex
72 self.x = x
73 self.y = y
74 self.z = z
76 def Copy(self) -> "Point3D[Coordinate]": # TODO: Python 3.11: -> Self:
77 """
78 Create a new 3D-point as a copy of this 3D point.
80 :return: Copy of this 3D-point.
82 .. seealso::
84 :meth:`+ operator <__add__>`
85 Create a new 3D-point moved by a positive 3D-offset.
86 :meth:`- operator <__sub__>`
87 Create a new 3D-point moved by a negative 3D-offset.
88 """
89 return self.__class__(self.x, self.y, self.z)
91 def ToTuple(self) -> Tuple[Coordinate, Coordinate, Coordinate]:
92 """
93 Convert this 3D-Point to a simple 3-element tuple.
95 :return: ``(x, y, z)`` tuple.
96 """
97 return self.x, self.y, self.z
99 def __add__(self, other: Any) -> "Point3D[Coordinate]":
100 """
101 Adds a 3D-offset to this 3D-point and creates a new 3D-point.
103 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`.
104 :return: A new 3D-point shifted by the 3D-offset.
105 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
106 """
107 if isinstance(other, Offset3D):
108 return self.__class__(
109 self.x + other.xOffset,
110 self.y + other.yOffset,
111 self.z + other.zOffset
112 )
113 elif isinstance(other, tuple):
114 return self.__class__(
115 self.x + other[0],
116 self.y + other[1],
117 self.z + other[2]
118 )
119 else:
120 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
121 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
122 raise ex
124 def __iadd__(self, other: Any) -> "Point3D[Coordinate]": # TODO: Python 3.11: -> Self:
125 """
126 Adds a 3D-offset to this 3D-point (inplace).
128 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`.
129 :return: This 3D-point.
130 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
131 """
132 if isinstance(other, Offset3D):
133 self.x += other.xOffset
134 self.y += other.yOffset
135 self.z += other.zOffset
136 elif isinstance(other, tuple):
137 self.x += other[0]
138 self.y += other[1]
139 self.z += other[2]
140 else:
141 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
142 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
143 raise ex
145 return self
147 def __sub__(self, other: Any) -> Union["Offset3D[Coordinate]", "Point3D[Coordinate]"]:
148 """
149 Subtract two 3D-Points from each other and create a new 3D-offset.
151 :param other: A 3D-point as :class:`Point3D`.
152 :return: A new 3D-offset representing the distance between these two points.
153 :raises TypeError: If parameter 'other' is not a :class:`Point3D`.
154 """
155 if isinstance(other, Point3D):
156 return Offset3D(
157 self.x - other.x,
158 self.y - other.y,
159 self.z - other.z
160 )
161 else:
162 ex = TypeError(f"Parameter 'other' is not of type Point3D.")
163 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
164 raise ex
166 def __isub__(self, other: Any) -> "Point3D[Coordinate]": # TODO: Python 3.11: -> Self:
167 """
168 Subtracts a 3D-offset to this 3D-point (inplace).
170 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`.
171 :return: This 3D-point.
172 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
173 """
174 if isinstance(other, Offset3D):
175 self.x -= other.xOffset
176 self.y -= other.yOffset
177 self.z -= other.zOffset
178 elif isinstance(other, tuple):
179 self.x -= other[0]
180 self.y -= other[1]
181 self.z -= other[2]
182 else:
183 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
184 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
185 raise ex
187 return self
189 def __repr__(self) -> str:
190 """
191 Returns the 3D point's string representation.
193 :returns: The string representation of the 3D point.
194 """
195 return f"Point3D({self.x}, {self.y}, {self.z})"
197 def __str__(self) -> str:
198 """
199 Returns the 3D point's string equivalent.
201 :returns: The string equivalent of the 3D point.
202 """
203 return f"({self.x}, {self.y}, {self.z})"
206@export
207class Origin3D(Point3D[Coordinate], Generic[Coordinate]):
208 """An implementation of a 3D cartesian origin."""
210 def __init__(self) -> None:
211 """
212 Initializes a 3-dimensional origin.
213 """
214 super().__init__(0, 0, 0)
216 def Copy(self) -> "Origin3D[Coordinate]": # TODO: Python 3.11: -> Self:
217 """
218 :raises RuntimeError: Because an origin can't be copied.
219 """
220 raise RuntimeError(f"An origin can't be copied.")
222 def __repr__(self) -> str:
223 """
224 Returns the 3D origin's string representation.
226 :returns: The string representation of the 3D origin.
227 """
228 return f"Origin3D({self.x}, {self.y}, {self.z})"
231@export
232class Offset3D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
233 """An implementation of a 3D cartesian offset."""
235 xOffset: Coordinate #: The x-direction offset
236 yOffset: Coordinate #: The y-direction offset
237 zOffset: Coordinate #: The z-direction offset
239 def __init__(self, xOffset: Coordinate, yOffset: Coordinate, zOffset: Coordinate) -> None:
240 """
241 Initializes a 3-dimensional offset.
243 :param xOffset: x-direction offset.
244 :param yOffset: y-direction offset.
245 :param zOffset: z-direction offset.
246 :raises TypeError: If x/y/z-offset is not of type integer or float.
247 """
248 if not isinstance(xOffset, (int, float)):
249 ex = TypeError(f"Parameter 'xOffset' is not of type integer or float.")
250 ex.add_note(f"Got type '{getFullyQualifiedName(xOffset)}'.")
251 raise ex
252 if not isinstance(yOffset, (int, float)):
253 ex = TypeError(f"Parameter 'yOffset' is not of type integer or float.")
254 ex.add_note(f"Got type '{getFullyQualifiedName(yOffset)}'.")
255 raise ex
256 if not isinstance(zOffset, (int, float)):
257 ex = TypeError(f"Parameter 'zOffset' is not of type integer or float.")
258 ex.add_note(f"Got type '{getFullyQualifiedName(zOffset)}'.")
259 raise ex
261 self.xOffset = xOffset
262 self.yOffset = yOffset
263 self.zOffset = zOffset
265 def Copy(self) -> "Offset3D[Coordinate]": # TODO: Python 3.11: -> Self:
266 """
267 Create a new 3D-offset as a copy of this 3D-offset.
269 :returns: Copy of this 3D-offset.
271 .. seealso::
273 :meth:`+ operator <__add__>`
274 Create a new 3D-offset moved by a positive 3D-offset.
275 :meth:`- operator <__sub__>`
276 Create a new 3D-offset moved by a negative 3D-offset.
277 """
278 return self.__class__(self.xOffset, self.yOffset, self.zOffset)
280 def ToTuple(self) -> Tuple[Coordinate, Coordinate, Coordinate]:
281 """
282 Convert this 3D-offset to a simple 3-element tuple.
284 :returns: ``(x, y, z)`` tuple.
285 """
286 return self.xOffset, self.yOffset, self.zOffset
288 def __eq__(self, other) -> bool:
289 """
290 Compare two 3D-offsets for equality.
292 :param other: Parameter to compare against.
293 :returns: ``True``, if both 3D-offsets are equal.
294 :raises TypeError: If parameter ``other`` is not of type :class:`Offset3D` or :class:`tuple`.
295 """
296 if isinstance(other, Offset3D):
297 return self.xOffset == other.xOffset and self.yOffset == other.yOffset and self.zOffset == other.zOffset
298 elif isinstance(other, tuple):
299 return self.xOffset == other[0] and self.yOffset == other[1] and self.zOffset == other[2]
300 else:
301 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
302 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
303 raise ex
305 def __ne__(self, other) -> bool:
306 """
307 Compare two 3D-offsets for inequality.
309 :param other: Parameter to compare against.
310 :returns: ``True``, if both 3D-offsets are unequal.
311 :raises TypeError: If parameter ``other`` is not of type :class:`Offset3D` or :class:`tuple`.
312 """
313 return not self.__eq__(other)
315 def __neg__(self) -> "Offset3D[Coordinate]":
316 """
317 Negate all components of this 3D-offset and create a new 3D-offset.
319 :returns: 3D-offset with negated offset components.
320 """
321 return self.__class__(
322 -self.xOffset,
323 -self.yOffset,
324 -self.zOffset
325 )
327 def __add__(self, other: Any) -> "Offset3D[Coordinate]":
328 """
329 Adds a 3D-offset to this 3D-offset and creates a new 3D-offset.
331 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`.
332 :returns: A new 3D-offset extended by the 3D-offset.
333 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
334 """
335 if isinstance(other, Offset3D):
336 return self.__class__(
337 self.xOffset + other.xOffset,
338 self.yOffset + other.yOffset,
339 self.zOffset + other.zOffset
340 )
341 elif isinstance(other, tuple):
342 return self.__class__(
343 self.xOffset + other[0],
344 self.yOffset + other[1],
345 self.zOffset + other[2]
346 )
347 else:
348 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
349 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
350 raise ex
352 def __iadd__(self, other: Any) -> "Offset3D[Coordinate]": # TODO: Python 3.11: -> Self:
353 """
354 Adds a 3D-offset to this 3D-offset (inplace).
356 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`.
357 :returns: This 3D-point.
358 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
359 """
360 if isinstance(other, Offset3D):
361 self.xOffset += other.xOffset
362 self.yOffset += other.yOffset
363 self.zOffset += other.zOffset
364 elif isinstance(other, tuple):
365 self.xOffset += other[0]
366 self.yOffset += other[1]
367 self.zOffset += other[2]
368 else:
369 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
370 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
371 raise ex
373 return self
375 def __sub__(self, other: Any) -> "Offset3D[Coordinate]":
376 """
377 Subtracts a 3D-offset from this 3D-offset and creates a new 3D-offset.
379 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`.
380 :returns: A new 3D-offset reduced by the 3D-offset.
381 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
382 """
383 if isinstance(other, Offset3D):
384 return self.__class__(
385 self.xOffset - other.xOffset,
386 self.yOffset - other.yOffset,
387 self.zOffset - other.zOffset
388 )
389 elif isinstance(other, tuple):
390 return self.__class__(
391 self.xOffset - other[0],
392 self.yOffset - other[1],
393 self.zOffset - other[2]
394 )
395 else:
396 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
397 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
398 raise ex
400 def __isub__(self, other: Any) -> "Offset3D[Coordinate]": # TODO: Python 3.11: -> Self:
401 """
402 Subtracts a 3D-offset from this 3D-offset (inplace).
404 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`.
405 :returns: This 3D-point.
406 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
407 """
408 if isinstance(other, Offset3D):
409 self.xOffset -= other.xOffset
410 self.yOffset -= other.yOffset
411 self.zOffset -= other.zOffset
412 elif isinstance(other, tuple):
413 self.xOffset -= other[0]
414 self.yOffset -= other[1]
415 self.zOffset -= other[2]
416 else:
417 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
418 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
419 raise ex
421 return self
423 def __repr__(self) -> str:
424 """
425 Returns the 3D offset's string representation.
427 :returns: The string representation of the 3D offset.
428 """
429 return f"Offset3D({self.xOffset}, {self.yOffset}, {self.zOffset})"
431 def __str__(self) -> str:
432 """
433 Returns the 3D offset's string equivalent.
435 :returns: The string equivalent of the 3D offset.
436 """
437 return f"({self.xOffset}, {self.yOffset}, {self.zOffset})"
440@export
441class Size3D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
442 """An implementation of a 3D cartesian size."""
444 width: Coordinate #: width in x-direction.
445 height: Coordinate #: height in y-direction.
446 depth: Coordinate #: depth in z-direction.
448 def __init__(self, width: Coordinate, height: Coordinate, depth: Coordinate) -> None:
449 """
450 Initializes a 2-dimensional size.
452 :param width: width in x-direction.
453 :param height: height in y-direction.
454 :param depth: depth in z-direction.
455 :raises TypeError: If width/height/depth is not of type integer or float.
456 """
457 if not isinstance(width, (int, float)):
458 ex = TypeError(f"Parameter 'width' is not of type integer or float.")
459 ex.add_note(f"Got type '{getFullyQualifiedName(width)}'.")
460 raise ex
461 if not isinstance(height, (int, float)):
462 ex = TypeError(f"Parameter 'height' is not of type integer or float.")
463 ex.add_note(f"Got type '{getFullyQualifiedName(height)}'.")
464 raise ex
465 if not isinstance(depth, (int, float)):
466 ex = TypeError(f"Parameter 'depth' is not of type integer or float.")
467 ex.add_note(f"Got type '{getFullyQualifiedName(depth)}'.")
468 raise ex
470 self.width = width
471 self.height = height
472 self.depth = depth
474 def Copy(self) -> "Size3D[Coordinate]": # TODO: Python 3.11: -> Self:
475 """
476 Create a new 3D-size as a copy of this 3D-size.
478 :returns: Copy of this 3D-size.
479 """
480 return self.__class__(self.width, self.height, self.depth)
482 def ToTuple(self) -> Tuple[Coordinate, Coordinate, Coordinate]:
483 """
484 Convert this 3D-size to a simple 3-element tuple.
486 :return: ``(width, height, depth)`` tuple.
487 """
488 return self.width, self.height, self.depth
490 def __repr__(self) -> str:
491 """
492 Returns the 3D size's string representation.
494 :returns: The string representation of the 3D size.
495 """
496 return f"Size3D({self.width}, {self.height}, {self.depth})"
498 def __str__(self) -> str:
499 """
500 Returns the 3D size's string equivalent.
502 :returns: The string equivalent of the 3D size.
503 """
504 return f"({self.width}, {self.height}, {self.depth})"
507@export
508class Segment3D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
509 """An implementation of a 3D cartesian segment."""
511 start: Point3D[Coordinate] #: Start point of a segment.
512 end: Point3D[Coordinate] #: End point of a segment.
514 def __init__(self, start: Point3D[Coordinate], end: Point3D[Coordinate], copyPoints: bool = True) -> None:
515 """
516 Initializes a 3-dimensional segment.
518 :param start: Start point of the segment.
519 :param end: End point of the segment.
520 :raises TypeError: If start/end is not of type Point3D.
521 """
522 if not isinstance(start, Point3D): 522 ↛ 523line 522 didn't jump to line 523 because the condition on line 522 was never true
523 ex = TypeError(f"Parameter 'start' is not of type Point3D.")
524 ex.add_note(f"Got type '{getFullyQualifiedName(start)}'.")
525 raise ex
526 if not isinstance(end, Point3D): 526 ↛ 527line 526 didn't jump to line 527 because the condition on line 526 was never true
527 ex = TypeError(f"Parameter 'end' is not of type Point3D.")
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 """
573 Returns the 3D line segment's string representation.
575 :returns: The string representation of the 3D line segment.
576 """
577 return f"LineSegment3D({self.start}, {self.end})"
579 def __str__(self) -> str:
580 """
581 Returns the 3D line segment's string equivalent.
583 :returns: The string equivalent of the 3D line segment.
584 """
585 return f"({self.start} → {self.end})"