Coverage for pyTooling / Cartesian2D / __init__.py: 93%
204 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-21 22:22 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-21 22:22 +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 2D cartesian data structures for Python."""
33from math import sqrt, acos
34from typing import TypeVar, 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
41except (ImportError, ModuleNotFoundError): # pragma: no cover
42 print("[pyTooling.Cartesian2D] Could not import from 'pyTooling.*'!")
44 try:
45 from Decorators import readonly, export
46 from Exceptions import ToolingException
47 from MetaClasses import ExtendedType
48 from Common import getFullyQualifiedName
49 except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover
50 print("[pyTooling.Cartesian2D] Could not import directly!")
51 raise ex
54Coordinate = TypeVar("Coordinate", bound=Union[int, float])
57@export
58class Point2D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
59 """An implementation of a 2D cartesian point."""
61 x: Coordinate #: The x-direction coordinate.
62 y: Coordinate #: The y-direction coordinate.
64 def __init__(self, x: Coordinate, y: Coordinate) -> None:
65 """
66 Initializes a 2-dimensional point.
68 :param x: X-coordinate.
69 :param y: Y-coordinate.
70 :raises TypeError: If x/y-coordinate is not of type integer or float.
71 """
72 if not isinstance(x, (int, float)):
73 ex = TypeError(f"Parameter 'x' is not of type integer or float.")
74 ex.add_note(f"Got type '{getFullyQualifiedName(x)}'.")
75 raise ex
76 if not isinstance(y, (int, float)):
77 ex = TypeError(f"Parameter 'y' is not of type integer or float.")
78 ex.add_note(f"Got type '{getFullyQualifiedName(y)}'.")
79 raise ex
81 self.x = x
82 self.y = y
84 def Copy(self) -> "Point2D[Coordinate]": # TODO: Python 3.11: -> Self:
85 """
86 Create a new 2D-point as a copy of this 2D point.
88 :returns: Copy of this 2D-point.
90 .. seealso::
92 :meth:`+ operator <__add__>`
93 Create a new 2D-point moved by a positive 2D-offset.
94 :meth:`- operator <__sub__>`
95 Create a new 2D-point moved by a negative 2D-offset.
96 """
97 return self.__class__(self.x, self.y)
99 def ToTuple(self) -> Tuple[Coordinate, Coordinate]:
100 """
101 Convert this 2D-Point to a simple 2-element tuple.
103 :returns: ``(x, y)`` tuple.
104 """
105 return self.x, self.y
107 def __add__(self, other: Any) -> "Point2D[Coordinate]":
108 """
109 Adds a 2D-offset to this 2D-point and creates a new 2D-point.
111 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`.
112 :returns: A new 2D-point shifted by the 2D-offset.
113 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`.
114 """
115 if isinstance(other, Offset2D):
116 return self.__class__(
117 self.x + other.xOffset,
118 self.y + other.yOffset
119 )
120 elif isinstance(other, tuple):
121 return self.__class__(
122 self.x + other[0],
123 self.y + other[1]
124 )
125 else:
126 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
127 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
128 raise ex
130 def __iadd__(self, other: Any) -> "Point2D[Coordinate]": # TODO: Python 3.11: -> Self:
131 """
132 Adds a 2D-offset to this 2D-point (inplace).
134 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`.
135 :returns: This 2D-point.
136 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`.
137 """
138 if isinstance(other, Offset2D):
139 self.x += other.xOffset
140 self.y += other.yOffset
141 elif isinstance(other, tuple):
142 self.x += other[0]
143 self.y += other[1]
144 else:
145 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
146 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
147 raise ex
149 return self
151 def __sub__(self, other: Any) -> Union["Offset2D[Coordinate]", "Point2D[Coordinate]"]:
152 """
153 Subtract two 2D-Points from each other and create a new 2D-offset.
155 :param other: A 2D-point as :class:`Point2D`.
156 :returns: A new 2D-offset representing the distance between these two points.
157 :raises TypeError: If parameter 'other' is not a :class:`Point2D`.
158 """
159 if isinstance(other, Point2D):
160 return Offset2D(
161 self.x - other.x,
162 self.y - other.y
163 )
164 else:
165 ex = TypeError(f"Parameter 'other' is not of type Point2D.")
166 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
167 raise ex
169 def __isub__(self, other: Any) -> "Point2D[Coordinate]": # TODO: Python 3.11: -> Self:
170 """
171 Subtracts a 2D-offset to this 2D-point (inplace).
173 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`.
174 :returns: This 2D-point.
175 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`.
176 """
177 if isinstance(other, Offset2D):
178 self.x -= other.xOffset
179 self.y -= other.yOffset
180 elif isinstance(other, tuple):
181 self.x -= other[0]
182 self.y -= other[1]
183 else:
184 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
185 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
186 raise ex
188 return self
190 def __repr__(self) -> str:
191 """
192 Returns the 2D point's string representation.
194 :returns: The string representation of the 2D point.
195 """
196 return f"Point2D({self.x}, {self.y})"
198 def __str__(self) -> str:
199 """
200 Returns the 2D point's string equivalent.
202 :returns: The string equivalent of the 2D point.
203 """
204 return f"({self.x}, {self.y})"
207@export
208class Origin2D(Point2D[Coordinate], Generic[Coordinate]):
209 """An implementation of a 2D cartesian origin."""
211 def __init__(self) -> None:
212 """
213 Initializes a 2-dimensional origin.
214 """
215 super().__init__(0, 0)
217 def Copy(self) -> "Origin2D[Coordinate]": # TODO: Python 3.11: -> Self:
218 """
219 :raises RuntimeError: Because an origin can't be copied.
220 """
221 raise RuntimeError(f"An origin can't be copied.")
223 def __repr__(self) -> str:
224 """
225 Returns the 2D origin's string representation.
227 :returns: The string representation of the 2D origin.
228 """
229 return f"Origin2D({self.x}, {self.y})"
232@export
233class Offset2D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
234 """An implementation of a 2D cartesian offset."""
236 xOffset: Coordinate #: The x-direction offset
237 yOffset: Coordinate #: The y-direction offset
239 def __init__(self, xOffset: Coordinate, yOffset: Coordinate) -> None:
240 """
241 Initializes a 2-dimensional offset.
243 :param xOffset: x-direction offset.
244 :param yOffset: y-direction offset.
245 :raises TypeError: If x/y-offset is not of type integer or float.
246 """
247 if not isinstance(xOffset, (int, float)):
248 ex = TypeError(f"Parameter 'xOffset' is not of type integer or float.")
249 ex.add_note(f"Got type '{getFullyQualifiedName(xOffset)}'.")
250 raise ex
251 if not isinstance(yOffset, (int, float)):
252 ex = TypeError(f"Parameter 'yOffset' is not of type integer or float.")
253 ex.add_note(f"Got type '{getFullyQualifiedName(yOffset)}'.")
254 raise ex
256 self.xOffset = xOffset
257 self.yOffset = yOffset
259 def Copy(self) -> "Offset2D[Coordinate]": # TODO: Python 3.11: -> Self:
260 """
261 Create a new 2D-offset as a copy of this 2D-offset.
263 :returns: Copy of this 2D-offset.
265 .. seealso::
267 :meth:`+ operator <__add__>`
268 Create a new 2D-offset moved by a positive 2D-offset.
269 :meth:`- operator <__sub__>`
270 Create a new 2D-offset moved by a negative 2D-offset.
271 """
272 return self.__class__(self.xOffset, self.yOffset)
274 def ToTuple(self) -> Tuple[Coordinate, Coordinate]:
275 """
276 Convert this 2D-offset to a simple 2-element tuple.
278 :returns: ``(x, y)`` tuple.
279 """
280 return self.xOffset, self.yOffset
282 def __eq__(self, other) -> bool:
283 """
284 Compare two 2D-offsets for equality.
286 :param other: Parameter to compare against.
287 :returns: ``True``, if both 2D-offsets are equal.
288 :raises TypeError: If parameter ``other`` is not of type :class:`Offset2D` or :class:`tuple`.
289 """
290 if isinstance(other, Offset2D):
291 return self.xOffset == other.xOffset and self.yOffset == other.yOffset
292 elif isinstance(other, tuple):
293 return self.xOffset == other[0] and self.yOffset == other[1]
294 else:
295 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
296 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
297 raise ex
299 def __ne__(self, other) -> bool:
300 """
301 Compare two 2D-offsets for inequality.
303 :param other: Parameter to compare against.
304 :returns: ``True``, if both 2D-offsets are unequal.
305 :raises TypeError: If parameter ``other`` is not of type :class:`Offset2D` or :class:`tuple`.
306 """
307 return not self.__eq__(other)
309 def __neg__(self) -> "Offset2D[Coordinate]":
310 """
311 Negate all components of this 2D-offset and create a new 2D-offset.
313 :returns: 2D-offset with negated offset components.
314 """
315 return self.__class__(
316 -self.xOffset,
317 -self.yOffset
318 )
320 def __add__(self, other: Any) -> "Offset2D[Coordinate]":
321 """
322 Adds a 2D-offset to this 2D-offset and creates a new 2D-offset.
324 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`.
325 :returns: A new 2D-offset extended by the 2D-offset.
326 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`.
327 """
328 if isinstance(other, Offset2D):
329 return self.__class__(
330 self.xOffset + other.xOffset,
331 self.yOffset + other.yOffset
332 )
333 elif isinstance(other, tuple):
334 return self.__class__(
335 self.xOffset + other[0],
336 self.yOffset + other[1]
337 )
338 else:
339 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
340 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
341 raise ex
343 def __iadd__(self, other: Any) -> "Offset2D[Coordinate]": # TODO: Python 3.11: -> Self:
344 """
345 Adds a 2D-offset to this 2D-offset (inplace).
347 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`.
348 :returns: This 2D-point.
349 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`.
350 """
351 if isinstance(other, Offset2D):
352 self.xOffset += other.xOffset
353 self.yOffset += other.yOffset
354 elif isinstance(other, tuple):
355 self.xOffset += other[0]
356 self.yOffset += other[1]
357 else:
358 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
359 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
360 raise ex
362 return self
364 def __sub__(self, other: Any) -> "Offset2D[Coordinate]":
365 """
366 Subtracts a 2D-offset from this 2D-offset and creates a new 2D-offset.
368 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`.
369 :returns: A new 2D-offset reduced by the 2D-offset.
370 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`.
371 """
372 if isinstance(other, Offset2D):
373 return self.__class__(
374 self.xOffset - other.xOffset,
375 self.yOffset - other.yOffset
376 )
377 elif isinstance(other, tuple):
378 return self.__class__(
379 self.xOffset - other[0],
380 self.yOffset - other[1]
381 )
382 else:
383 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
384 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
385 raise ex
387 def __isub__(self, other: Any) -> "Offset2D[Coordinate]": # TODO: Python 3.11: -> Self:
388 """
389 Subtracts a 2D-offset from this 2D-offset (inplace).
391 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`.
392 :returns: This 2D-point.
393 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`.
394 """
395 if isinstance(other, Offset2D):
396 self.xOffset -= other.xOffset
397 self.yOffset -= other.yOffset
398 elif isinstance(other, tuple):
399 self.xOffset -= other[0]
400 self.yOffset -= other[1]
401 else:
402 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
403 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
404 raise ex
406 return self
408 def __repr__(self) -> str:
409 """
410 Returns the 2D offset's string representation.
412 :returns: The string representation of the 2D offset.
413 """
414 return f"Offset2D({self.xOffset}, {self.yOffset})"
416 def __str__(self) -> str:
417 """
418 Returns the 2D offset's string equivalent.
420 :returns: The string equivalent of the 2D offset.
421 """
422 return f"({self.xOffset}, {self.yOffset})"
425@export
426class Size2D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
427 """An implementation of a 2D cartesian size."""
429 width: Coordinate #: width in x-direction.
430 height: Coordinate #: height in y-direction.
432 def __init__(self, width: Coordinate, height: Coordinate) -> None:
433 """
434 Initializes a 2-dimensional size.
436 :param width: width in x-direction.
437 :param height: height in y-direction.
438 :raises TypeError: If width/height is not of type integer or float.
439 """
440 if not isinstance(width, (int, float)):
441 ex = TypeError(f"Parameter 'width' is not of type integer or float.")
442 ex.add_note(f"Got type '{getFullyQualifiedName(width)}'.")
443 raise ex
444 if not isinstance(height, (int, float)):
445 ex = TypeError(f"Parameter 'height' is not of type integer or float.")
446 ex.add_note(f"Got type '{getFullyQualifiedName(height)}'.")
447 raise ex
449 self.width = width
450 self.height = height
452 def Copy(self) -> "Size2D": # TODO: Python 3.11: -> Self:
453 """
454 Create a new 2D-size as a copy of this 2D-size.
456 :returns: Copy of this 2D-size.
457 """
458 return self.__class__(self.width, self.height)
460 def ToTuple(self) -> Tuple[Coordinate, Coordinate]:
461 """
462 Convert this 2D-size to a simple 2-element tuple.
464 :return: ``(width, height)`` tuple.
465 """
466 return self.width, self.height
468 def __repr__(self) -> str:
469 """
470 Returns the 2D size's string representation.
472 :returns: The string representation of the 2D size.
473 """
474 return f"Size2D({self.width}, {self.height})"
476 def __str__(self) -> str:
477 """
478 Returns the 2D size's string equivalent.
480 :returns: The string equivalent of the 2D size.
481 """
482 return f"({self.width}, {self.height})"
485@export
486class Segment2D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
487 """An implementation of a 2D cartesian segment."""
489 start: Point2D[Coordinate] #: Start point of a segment.
490 end: Point2D[Coordinate] #: End point of a segment.
492 def __init__(self, start: Point2D[Coordinate], end: Point2D[Coordinate], copyPoints: bool = True) -> None:
493 """
494 Initializes a 2-dimensional segment.
496 :param start: Start point of the segment.
497 :param end: End point of the segment.
498 :raises TypeError: If start/end is not of type Point2D.
499 """
500 if not isinstance(start, Point2D): 500 ↛ 501line 500 didn't jump to line 501 because the condition on line 500 was never true
501 ex = TypeError(f"Parameter 'start' is not of type Point2D.")
502 ex.add_note(f"Got type '{getFullyQualifiedName(start)}'.")
503 raise ex
504 if not isinstance(end, Point2D): 504 ↛ 505line 504 didn't jump to line 505 because the condition on line 504 was never true
505 ex = TypeError(f"Parameter 'end' is not of type Point2D.")
506 ex.add_note(f"Got type '{getFullyQualifiedName(end)}'.")
507 raise ex
509 self.start = start.Copy() if copyPoints else start
510 self.end = end.Copy() if copyPoints else end
513@export
514class LineSegment2D(Segment2D[Coordinate], Generic[Coordinate]):
515 """An implementation of a 2D cartesian line segment."""
517 @readonly
518 def Length(self) -> float:
519 """
520 Read-only property to return the Euclidean distance between start and end point.
522 :return: Euclidean distance between start and end point
523 """
524 return sqrt((self.end.x - self.start.x) ** 2 + (self.end.x - self.start.x) ** 2)
526 def AngleTo(self, other: "LineSegment2D[Coordinate]") -> float:
527 vectorA = self.ToOffset()
528 vectorB = other.ToOffset()
529 scalarProductAB = vectorA.xOffset * vectorB.xOffset + vectorA.yOffset * vectorB.yOffset
531 return acos(scalarProductAB / (abs(self.Length) * abs(other.Length)))
533 def ToOffset(self) -> Offset2D[Coordinate]:
534 """
535 Convert this 2D line segment to a 2D-offset.
537 :return: 2D-offset as :class:`Offset2D`
538 """
539 return self.end - self.start
541 def ToTuple(self) -> Tuple[Tuple[Coordinate, Coordinate], Tuple[Coordinate, Coordinate]]:
542 """
543 Convert this 2D line segment to a simple 2-element tuple of 2D-point tuples.
545 :return: ``((x1, y1), (x2, y2))`` tuple.
546 """
547 return self.start.ToTuple(), self.end.ToTuple()
549 def __repr__(self) -> str:
550 """
551 Returns the 2D line segment's string representation.
553 :returns: The string representation of the 2D line segment.
554 """
555 return f"LineSegment2D({self.start}, {self.end})"
557 def __str__(self) -> str:
558 """
559 Returns the 2D line segment's string equivalent.
561 :returns: The string equivalent of the 2D line segment.
562 """
563 return f"({self.start} → {self.end})"