Coverage for pyTooling / Cartesian2D / __init__.py: 93%
202 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 2D cartesian data structures for Python."""
33from math import sqrt, acos
34from typing import TypeVar, Union, Generic, Any, Tuple
36from pyTooling.Decorators import readonly, export
37from pyTooling.MetaClasses import ExtendedType
38from pyTooling.Common import getFullyQualifiedName
41Coordinate = TypeVar("Coordinate", bound=Union[int, float])
44@export
45class Point2D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
46 """An implementation of a 2D cartesian point."""
48 x: Coordinate #: The x-direction coordinate.
49 y: Coordinate #: The y-direction coordinate.
51 def __init__(self, x: Coordinate, y: Coordinate) -> None:
52 """
53 Initializes a 2-dimensional point.
55 :param x: X-coordinate.
56 :param y: Y-coordinate.
57 :raises TypeError: If x/y-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
68 self.x = x
69 self.y = y
71 def Copy(self) -> "Point2D[Coordinate]": # TODO: Python 3.11: -> Self:
72 """
73 Create a new 2D-point as a copy of this 2D point.
75 :returns: Copy of this 2D-point.
77 .. seealso::
79 :meth:`+ operator <__add__>`
80 Create a new 2D-point moved by a positive 2D-offset.
81 :meth:`- operator <__sub__>`
82 Create a new 2D-point moved by a negative 2D-offset.
83 """
84 return self.__class__(self.x, self.y)
86 def ToTuple(self) -> Tuple[Coordinate, Coordinate]:
87 """
88 Convert this 2D-Point to a simple 2-element tuple.
90 :returns: ``(x, y)`` tuple.
91 """
92 return self.x, self.y
94 def __add__(self, other: Any) -> "Point2D[Coordinate]":
95 """
96 Adds a 2D-offset to this 2D-point and creates a new 2D-point.
98 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`.
99 :returns: A new 2D-point shifted by the 2D-offset.
100 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`.
101 """
102 if isinstance(other, Offset2D):
103 return self.__class__(
104 self.x + other.xOffset,
105 self.y + other.yOffset
106 )
107 elif isinstance(other, tuple):
108 return self.__class__(
109 self.x + other[0],
110 self.y + other[1]
111 )
112 else:
113 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
114 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
115 raise ex
117 def __iadd__(self, other: Any) -> "Point2D[Coordinate]": # TODO: Python 3.11: -> Self:
118 """
119 Adds a 2D-offset to this 2D-point (inplace).
121 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`.
122 :returns: This 2D-point.
123 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`.
124 """
125 if isinstance(other, Offset2D):
126 self.x += other.xOffset
127 self.y += other.yOffset
128 elif isinstance(other, tuple):
129 self.x += other[0]
130 self.y += other[1]
131 else:
132 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
133 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
134 raise ex
136 return self
138 def __sub__(self, other: Any) -> Union["Offset2D[Coordinate]", "Point2D[Coordinate]"]:
139 """
140 Subtract two 2D-Points from each other and create a new 2D-offset.
142 :param other: A 2D-point as :class:`Point2D`.
143 :returns: A new 2D-offset representing the distance between these two points.
144 :raises TypeError: If parameter 'other' is not a :class:`Point2D`.
145 """
146 if isinstance(other, Point2D):
147 return Offset2D(
148 self.x - other.x,
149 self.y - other.y
150 )
151 else:
152 ex = TypeError(f"Parameter 'other' is not of type Point2D.")
153 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
154 raise ex
156 def __isub__(self, other: Any) -> "Point2D[Coordinate]": # TODO: Python 3.11: -> Self:
157 """
158 Subtracts a 2D-offset to this 2D-point (inplace).
160 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`.
161 :returns: This 2D-point.
162 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`.
163 """
164 if isinstance(other, Offset2D):
165 self.x -= other.xOffset
166 self.y -= other.yOffset
167 elif isinstance(other, tuple):
168 self.x -= other[0]
169 self.y -= other[1]
170 else:
171 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
172 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
173 raise ex
175 return self
177 def __repr__(self) -> str:
178 """
179 Returns the 2D point's string representation.
181 :returns: The string representation of the 2D point.
182 """
183 return f"Point2D({self.x}, {self.y})"
185 def __str__(self) -> str:
186 """
187 Returns the 2D point's string equivalent.
189 :returns: The string equivalent of the 2D point.
190 """
191 return f"({self.x}, {self.y})"
194@export
195class Origin2D(Point2D[Coordinate], Generic[Coordinate]):
196 """An implementation of a 2D cartesian origin."""
198 def __init__(self) -> None:
199 """
200 Initializes a 2-dimensional origin.
201 """
202 super().__init__(0, 0)
204 def Copy(self) -> "Origin2D[Coordinate]": # TODO: Python 3.11: -> Self:
205 """
206 :raises RuntimeError: Because an origin can't be copied.
207 """
208 raise RuntimeError(f"An origin can't be copied.")
210 def __repr__(self) -> str:
211 """
212 Returns the 2D origin's string representation.
214 :returns: The string representation of the 2D origin.
215 """
216 return f"Origin2D({self.x}, {self.y})"
219@export
220class Offset2D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
221 """An implementation of a 2D cartesian offset."""
223 xOffset: Coordinate #: The x-direction offset
224 yOffset: Coordinate #: The y-direction offset
226 def __init__(self, xOffset: Coordinate, yOffset: Coordinate) -> None:
227 """
228 Initializes a 2-dimensional offset.
230 :param xOffset: x-direction offset.
231 :param yOffset: y-direction offset.
232 :raises TypeError: If x/y-offset is not of type integer or float.
233 """
234 if not isinstance(xOffset, (int, float)):
235 ex = TypeError(f"Parameter 'xOffset' is not of type integer or float.")
236 ex.add_note(f"Got type '{getFullyQualifiedName(xOffset)}'.")
237 raise ex
238 if not isinstance(yOffset, (int, float)):
239 ex = TypeError(f"Parameter 'yOffset' is not of type integer or float.")
240 ex.add_note(f"Got type '{getFullyQualifiedName(yOffset)}'.")
241 raise ex
243 self.xOffset = xOffset
244 self.yOffset = yOffset
246 def Copy(self) -> "Offset2D[Coordinate]": # TODO: Python 3.11: -> Self:
247 """
248 Create a new 2D-offset as a copy of this 2D-offset.
250 :returns: Copy of this 2D-offset.
252 .. seealso::
254 :meth:`+ operator <__add__>`
255 Create a new 2D-offset moved by a positive 2D-offset.
256 :meth:`- operator <__sub__>`
257 Create a new 2D-offset moved by a negative 2D-offset.
258 """
259 return self.__class__(self.xOffset, self.yOffset)
261 def ToTuple(self) -> Tuple[Coordinate, Coordinate]:
262 """
263 Convert this 2D-offset to a simple 2-element tuple.
265 :returns: ``(x, y)`` tuple.
266 """
267 return self.xOffset, self.yOffset
269 def __eq__(self, other) -> bool:
270 """
271 Compare two 2D-offsets for equality.
273 :param other: Parameter to compare against.
274 :returns: ``True``, if both 2D-offsets are equal.
275 :raises TypeError: If parameter ``other`` is not of type :class:`Offset2D` or :class:`tuple`.
276 """
277 if isinstance(other, Offset2D):
278 return self.xOffset == other.xOffset and self.yOffset == other.yOffset
279 elif isinstance(other, tuple):
280 return self.xOffset == other[0] and self.yOffset == other[1]
281 else:
282 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
283 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
284 raise ex
286 def __ne__(self, other) -> bool:
287 """
288 Compare two 2D-offsets for inequality.
290 :param other: Parameter to compare against.
291 :returns: ``True``, if both 2D-offsets are unequal.
292 :raises TypeError: If parameter ``other`` is not of type :class:`Offset2D` or :class:`tuple`.
293 """
294 return not self.__eq__(other)
296 def __neg__(self) -> "Offset2D[Coordinate]":
297 """
298 Negate all components of this 2D-offset and create a new 2D-offset.
300 :returns: 2D-offset with negated offset components.
301 """
302 return self.__class__(
303 -self.xOffset,
304 -self.yOffset
305 )
307 def __add__(self, other: Any) -> "Offset2D[Coordinate]":
308 """
309 Adds a 2D-offset to this 2D-offset and creates a new 2D-offset.
311 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`.
312 :returns: A new 2D-offset extended by the 2D-offset.
313 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`.
314 """
315 if isinstance(other, Offset2D):
316 return self.__class__(
317 self.xOffset + other.xOffset,
318 self.yOffset + other.yOffset
319 )
320 elif isinstance(other, tuple):
321 return self.__class__(
322 self.xOffset + other[0],
323 self.yOffset + other[1]
324 )
325 else:
326 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
327 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
328 raise ex
330 def __iadd__(self, other: Any) -> "Offset2D[Coordinate]": # TODO: Python 3.11: -> Self:
331 """
332 Adds a 2D-offset to this 2D-offset (inplace).
334 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`.
335 :returns: This 2D-point.
336 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`.
337 """
338 if isinstance(other, Offset2D):
339 self.xOffset += other.xOffset
340 self.yOffset += other.yOffset
341 elif isinstance(other, tuple):
342 self.xOffset += other[0]
343 self.yOffset += other[1]
344 else:
345 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
346 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
347 raise ex
349 return self
351 def __sub__(self, other: Any) -> "Offset2D[Coordinate]":
352 """
353 Subtracts a 2D-offset from this 2D-offset and creates a new 2D-offset.
355 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`.
356 :returns: A new 2D-offset reduced by the 2D-offset.
357 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`.
358 """
359 if isinstance(other, Offset2D):
360 return self.__class__(
361 self.xOffset - other.xOffset,
362 self.yOffset - other.yOffset
363 )
364 elif isinstance(other, tuple):
365 return self.__class__(
366 self.xOffset - other[0],
367 self.yOffset - other[1]
368 )
369 else:
370 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
371 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
372 raise ex
374 def __isub__(self, other: Any) -> "Offset2D[Coordinate]": # TODO: Python 3.11: -> Self:
375 """
376 Subtracts a 2D-offset from this 2D-offset (inplace).
378 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`.
379 :returns: This 2D-point.
380 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`.
381 """
382 if isinstance(other, Offset2D):
383 self.xOffset -= other.xOffset
384 self.yOffset -= other.yOffset
385 elif isinstance(other, tuple):
386 self.xOffset -= other[0]
387 self.yOffset -= other[1]
388 else:
389 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
390 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
391 raise ex
393 return self
395 def __repr__(self) -> str:
396 """
397 Returns the 2D offset's string representation.
399 :returns: The string representation of the 2D offset.
400 """
401 return f"Offset2D({self.xOffset}, {self.yOffset})"
403 def __str__(self) -> str:
404 """
405 Returns the 2D offset's string equivalent.
407 :returns: The string equivalent of the 2D offset.
408 """
409 return f"({self.xOffset}, {self.yOffset})"
412@export
413class Size2D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
414 """An implementation of a 2D cartesian size."""
416 width: Coordinate #: width in x-direction.
417 height: Coordinate #: height in y-direction.
419 def __init__(self, width: Coordinate, height: Coordinate) -> None:
420 """
421 Initializes a 2-dimensional size.
423 :param width: width in x-direction.
424 :param height: height in y-direction.
425 :raises TypeError: If width/height is not of type integer or float.
426 """
427 if not isinstance(width, (int, float)):
428 ex = TypeError(f"Parameter 'width' is not of type integer or float.")
429 ex.add_note(f"Got type '{getFullyQualifiedName(width)}'.")
430 raise ex
431 if not isinstance(height, (int, float)):
432 ex = TypeError(f"Parameter 'height' is not of type integer or float.")
433 ex.add_note(f"Got type '{getFullyQualifiedName(height)}'.")
434 raise ex
436 self.width = width
437 self.height = height
439 def Copy(self) -> "Size2D": # TODO: Python 3.11: -> Self:
440 """
441 Create a new 2D-size as a copy of this 2D-size.
443 :returns: Copy of this 2D-size.
444 """
445 return self.__class__(self.width, self.height)
447 def ToTuple(self) -> Tuple[Coordinate, Coordinate]:
448 """
449 Convert this 2D-size to a simple 2-element tuple.
451 :return: ``(width, height)`` tuple.
452 """
453 return self.width, self.height
455 def __repr__(self) -> str:
456 """
457 Returns the 2D size's string representation.
459 :returns: The string representation of the 2D size.
460 """
461 return f"Size2D({self.width}, {self.height})"
463 def __str__(self) -> str:
464 """
465 Returns the 2D size's string equivalent.
467 :returns: The string equivalent of the 2D size.
468 """
469 return f"({self.width}, {self.height})"
472@export
473class Segment2D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
474 """An implementation of a 2D cartesian segment."""
476 start: Point2D[Coordinate] #: Start point of a segment.
477 end: Point2D[Coordinate] #: End point of a segment.
479 def __init__(self, start: Point2D[Coordinate], end: Point2D[Coordinate], copyPoints: bool = True) -> None:
480 """
481 Initializes a 2-dimensional segment.
483 :param start: Start point of the segment.
484 :param end: End point of the segment.
485 :raises TypeError: If start/end is not of type Point2D.
486 """
487 if not isinstance(start, Point2D): 487 ↛ 488line 487 didn't jump to line 488 because the condition on line 487 was never true
488 ex = TypeError(f"Parameter 'start' is not of type Point2D.")
489 ex.add_note(f"Got type '{getFullyQualifiedName(start)}'.")
490 raise ex
491 if not isinstance(end, Point2D): 491 ↛ 492line 491 didn't jump to line 492 because the condition on line 491 was never true
492 ex = TypeError(f"Parameter 'end' is not of type Point2D.")
493 ex.add_note(f"Got type '{getFullyQualifiedName(end)}'.")
494 raise ex
496 self.start = start.Copy() if copyPoints else start
497 self.end = end.Copy() if copyPoints else end
500@export
501class LineSegment2D(Segment2D[Coordinate], Generic[Coordinate]):
502 """An implementation of a 2D cartesian line segment."""
504 @readonly
505 def Length(self) -> float:
506 """
507 Read-only property to return the Euclidean distance between start and end point.
509 :return: Euclidean distance between start and end point
510 """
511 return sqrt((self.end.x - self.start.x) ** 2 + (self.end.x - self.start.x) ** 2)
513 def AngleTo(self, other: "LineSegment2D[Coordinate]") -> float:
514 vectorA = self.ToOffset()
515 vectorB = other.ToOffset()
516 scalarProductAB = vectorA.xOffset * vectorB.xOffset + vectorA.yOffset * vectorB.yOffset
518 return acos(scalarProductAB / (abs(self.Length) * abs(other.Length)))
520 def ToOffset(self) -> Offset2D[Coordinate]:
521 """
522 Convert this 2D line segment to a 2D-offset.
524 :return: 2D-offset as :class:`Offset2D`
525 """
526 return self.end - self.start
528 def ToTuple(self) -> Tuple[Tuple[Coordinate, Coordinate], Tuple[Coordinate, Coordinate]]:
529 """
530 Convert this 2D line segment to a simple 2-element tuple of 2D-point tuples.
532 :return: ``((x1, y1), (x2, y2))`` tuple.
533 """
534 return self.start.ToTuple(), self.end.ToTuple()
536 def __repr__(self) -> str:
537 """
538 Returns the 2D line segment's string representation.
540 :returns: The string representation of the 2D line segment.
541 """
542 return f"LineSegment2D({self.start}, {self.end})"
544 def __str__(self) -> str:
545 """
546 Returns the 2D line segment's string equivalent.
548 :returns: The string equivalent of the 2D line segment.
549 """
550 return f"({self.start} → {self.end})"