Coverage for pyTooling/Cartesian2D/__init__.py: 93%
205 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 2D cartesian data structures for Python."""
32from sys import version_info
34from math import sqrt, acos
35from typing import TypeVar, 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
42except (ImportError, ModuleNotFoundError): # pragma: no cover
43 print("[pyTooling.Cartesian2D] Could not import from 'pyTooling.*'!")
45 try:
46 from Decorators import readonly, export
47 from Exceptions import ToolingException
48 from MetaClasses import ExtendedType
49 from Common import getFullyQualifiedName
50 except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover
51 print("[pyTooling.Cartesian2D] Could not import directly!")
52 raise ex
55Coordinate = TypeVar("Coordinate", bound=Union[int, float])
58@export
59class Point2D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
60 """An implementation of a 2D cartesian point."""
62 x: Coordinate #: The x-direction coordinate.
63 y: Coordinate #: The y-direction coordinate.
65 def __init__(self, x: Coordinate, y: Coordinate) -> None:
66 """
67 Initializes a 2-dimensional point.
69 :param x: X-coordinate.
70 :param y: Y-coordinate.
71 :raises TypeError: If x/y-coordinate is not of type integer or float.
72 """
73 if not isinstance(x, (int, float)):
74 ex = TypeError(f"Parameter 'x' is not of type integer or float.")
75 ex.add_note(f"Got type '{getFullyQualifiedName(x)}'.")
76 raise ex
77 if not isinstance(y, (int, float)):
78 ex = TypeError(f"Parameter 'y' is not of type integer or float.")
79 ex.add_note(f"Got type '{getFullyQualifiedName(y)}'.")
80 raise ex
82 self.x = x
83 self.y = y
85 def Copy(self) -> "Point2D[Coordinate]": # TODO: Python 3.11: -> Self:
86 """
87 Create a new 2D-point as a copy of this 2D point.
89 :returns: Copy of this 2D-point.
91 .. seealso::
93 :meth:`+ operator <__add__>`
94 Create a new 2D-point moved by a positive 2D-offset.
95 :meth:`- operator <__sub__>`
96 Create a new 2D-point moved by a negative 2D-offset.
97 """
98 return self.__class__(self.x, self.y)
100 def ToTuple(self) -> Tuple[Coordinate, Coordinate]:
101 """
102 Convert this 2D-Point to a simple 2-element tuple.
104 :returns: ``(x, y)`` tuple.
105 """
106 return self.x, self.y
108 def __add__(self, other: Any) -> "Point2D[Coordinate]":
109 """
110 Adds a 2D-offset to this 2D-point and creates a new 2D-point.
112 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`.
113 :returns: A new 2D-point shifted by the 2D-offset.
114 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`.
115 """
116 if isinstance(other, Offset2D):
117 return self.__class__(
118 self.x + other.xOffset,
119 self.y + other.yOffset
120 )
121 elif isinstance(other, tuple):
122 return self.__class__(
123 self.x + other[0],
124 self.y + other[1]
125 )
126 else:
127 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
128 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
129 raise ex
131 def __iadd__(self, other: Any) -> "Point2D[Coordinate]": # TODO: Python 3.11: -> Self:
132 """
133 Adds a 2D-offset to this 2D-point (inplace).
135 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`.
136 :returns: This 2D-point.
137 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`.
138 """
139 if isinstance(other, Offset2D):
140 self.x += other.xOffset
141 self.y += other.yOffset
142 elif isinstance(other, tuple):
143 self.x += other[0]
144 self.y += other[1]
145 else:
146 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
147 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
148 raise ex
150 return self
152 def __sub__(self, other: Any) -> Union["Offset2D[Coordinate]", "Point2D[Coordinate]"]:
153 """
154 Subtract two 2D-Points from each other and create a new 2D-offset.
156 :param other: A 2D-point as :class:`Point2D`.
157 :returns: A new 2D-offset representing the distance between these two points.
158 :raises TypeError: If parameter 'other' is not a :class:`Point2D`.
159 """
160 if isinstance(other, Point2D):
161 return Offset2D(
162 self.x - other.x,
163 self.y - other.y
164 )
165 else:
166 ex = TypeError(f"Parameter 'other' is not of type Point2D.")
167 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
168 raise ex
170 def __isub__(self, other: Any) -> "Point2D[Coordinate]": # TODO: Python 3.11: -> Self:
171 """
172 Subtracts a 2D-offset to this 2D-point (inplace).
174 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`.
175 :returns: This 2D-point.
176 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`.
177 """
178 if isinstance(other, Offset2D):
179 self.x -= other.xOffset
180 self.y -= other.yOffset
181 elif isinstance(other, tuple):
182 self.x -= other[0]
183 self.y -= other[1]
184 else:
185 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
186 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
187 raise ex
189 return self
191 def __repr__(self) -> str:
192 """
193 Returns the 2D point's string representation.
195 :returns: The string representation of the 2D point.
196 """
197 return f"Point2D({self.x}, {self.y})"
199 def __str__(self) -> str:
200 """
201 Returns the 2D point's string equivalent.
203 :returns: The string equivalent of the 2D point.
204 """
205 return f"({self.x}, {self.y})"
208@export
209class Origin2D(Point2D[Coordinate], Generic[Coordinate]):
210 """An implementation of a 2D cartesian origin."""
212 def __init__(self) -> None:
213 """
214 Initializes a 2-dimensional origin.
215 """
216 super().__init__(0, 0)
218 def Copy(self) -> "Origin2D[Coordinate]": # TODO: Python 3.11: -> Self:
219 """
220 :raises RuntimeError: Because an origin can't be copied.
221 """
222 raise RuntimeError(f"An origin can't be copied.")
224 def __repr__(self) -> str:
225 """
226 Returns the 2D origin's string representation.
228 :returns: The string representation of the 2D origin.
229 """
230 return f"Origin2D({self.x}, {self.y})"
233@export
234class Offset2D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
235 """An implementation of a 2D cartesian offset."""
237 xOffset: Coordinate #: The x-direction offset
238 yOffset: Coordinate #: The y-direction offset
240 def __init__(self, xOffset: Coordinate, yOffset: Coordinate) -> None:
241 """
242 Initializes a 2-dimensional offset.
244 :param xOffset: x-direction offset.
245 :param yOffset: y-direction offset.
246 :raises TypeError: If x/y-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
257 self.xOffset = xOffset
258 self.yOffset = yOffset
260 def Copy(self) -> "Offset2D[Coordinate]": # TODO: Python 3.11: -> Self:
261 """
262 Create a new 2D-offset as a copy of this 2D-offset.
264 :returns: Copy of this 2D-offset.
266 .. seealso::
268 :meth:`+ operator <__add__>`
269 Create a new 2D-offset moved by a positive 2D-offset.
270 :meth:`- operator <__sub__>`
271 Create a new 2D-offset moved by a negative 2D-offset.
272 """
273 return self.__class__(self.xOffset, self.yOffset)
275 def ToTuple(self) -> Tuple[Coordinate, Coordinate]:
276 """
277 Convert this 2D-offset to a simple 2-element tuple.
279 :returns: ``(x, y)`` tuple.
280 """
281 return self.xOffset, self.yOffset
283 def __eq__(self, other) -> bool:
284 """
285 Compare two 2D-offsets for equality.
287 :param other: Parameter to compare against.
288 :returns: ``True``, if both 2D-offsets are equal.
289 :raises TypeError: If parameter ``other`` is not of type :class:`Offset2D` or :class:`tuple`.
290 """
291 if isinstance(other, Offset2D):
292 return self.xOffset == other.xOffset and self.yOffset == other.yOffset
293 elif isinstance(other, tuple):
294 return self.xOffset == other[0] and self.yOffset == other[1]
295 else:
296 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
297 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
298 raise ex
300 def __ne__(self, other) -> bool:
301 """
302 Compare two 2D-offsets for inequality.
304 :param other: Parameter to compare against.
305 :returns: ``True``, if both 2D-offsets are unequal.
306 :raises TypeError: If parameter ``other`` is not of type :class:`Offset2D` or :class:`tuple`.
307 """
308 return not self.__eq__(other)
310 def __neg__(self) -> "Offset2D[Coordinate]":
311 """
312 Negate all components of this 2D-offset and create a new 2D-offset.
314 :returns: 2D-offset with negated offset components.
315 """
316 return self.__class__(
317 -self.xOffset,
318 -self.yOffset
319 )
321 def __add__(self, other: Any) -> "Offset2D[Coordinate]":
322 """
323 Adds a 2D-offset to this 2D-offset and creates a new 2D-offset.
325 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`.
326 :returns: A new 2D-offset extended by the 2D-offset.
327 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`.
328 """
329 if isinstance(other, Offset2D):
330 return self.__class__(
331 self.xOffset + other.xOffset,
332 self.yOffset + other.yOffset
333 )
334 elif isinstance(other, tuple):
335 return self.__class__(
336 self.xOffset + other[0],
337 self.yOffset + other[1]
338 )
339 else:
340 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
341 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
342 raise ex
344 def __iadd__(self, other: Any) -> "Offset2D[Coordinate]": # TODO: Python 3.11: -> Self:
345 """
346 Adds a 2D-offset to this 2D-offset (inplace).
348 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`.
349 :returns: This 2D-point.
350 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`.
351 """
352 if isinstance(other, Offset2D):
353 self.xOffset += other.xOffset
354 self.yOffset += other.yOffset
355 elif isinstance(other, tuple):
356 self.xOffset += other[0]
357 self.yOffset += other[1]
358 else:
359 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
360 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
361 raise ex
363 return self
365 def __sub__(self, other: Any) -> "Offset2D[Coordinate]":
366 """
367 Subtracts a 2D-offset from this 2D-offset and creates a new 2D-offset.
369 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`.
370 :returns: A new 2D-offset reduced by the 2D-offset.
371 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`.
372 """
373 if isinstance(other, Offset2D):
374 return self.__class__(
375 self.xOffset - other.xOffset,
376 self.yOffset - other.yOffset
377 )
378 elif isinstance(other, tuple):
379 return self.__class__(
380 self.xOffset - other[0],
381 self.yOffset - other[1]
382 )
383 else:
384 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
385 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
386 raise ex
388 def __isub__(self, other: Any) -> "Offset2D[Coordinate]": # TODO: Python 3.11: -> Self:
389 """
390 Subtracts a 2D-offset from this 2D-offset (inplace).
392 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`.
393 :returns: This 2D-point.
394 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`.
395 """
396 if isinstance(other, Offset2D):
397 self.xOffset -= other.xOffset
398 self.yOffset -= other.yOffset
399 elif isinstance(other, tuple):
400 self.xOffset -= other[0]
401 self.yOffset -= other[1]
402 else:
403 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
404 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
405 raise ex
407 return self
409 def __repr__(self) -> str:
410 """
411 Returns the 2D offset's string representation.
413 :returns: The string representation of the 2D offset.
414 """
415 return f"Offset2D({self.xOffset}, {self.yOffset})"
417 def __str__(self) -> str:
418 """
419 Returns the 2D offset's string equivalent.
421 :returns: The string equivalent of the 2D offset.
422 """
423 return f"({self.xOffset}, {self.yOffset})"
426@export
427class Size2D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
428 """An implementation of a 2D cartesian size."""
430 width: Coordinate #: width in x-direction.
431 height: Coordinate #: height in y-direction.
433 def __init__(self, width: Coordinate, height: Coordinate) -> None:
434 """
435 Initializes a 2-dimensional size.
437 :param width: width in x-direction.
438 :param height: height in y-direction.
439 :raises TypeError: If width/height is not of type integer or float.
440 """
441 if not isinstance(width, (int, float)):
442 ex = TypeError(f"Parameter 'width' is not of type integer or float.")
443 ex.add_note(f"Got type '{getFullyQualifiedName(width)}'.")
444 raise ex
445 if not isinstance(height, (int, float)):
446 ex = TypeError(f"Parameter 'height' is not of type integer or float.")
447 ex.add_note(f"Got type '{getFullyQualifiedName(height)}'.")
448 raise ex
450 self.width = width
451 self.height = height
453 def Copy(self) -> "Size2D": # TODO: Python 3.11: -> Self:
454 """
455 Create a new 2D-size as a copy of this 2D-size.
457 :returns: Copy of this 2D-size.
458 """
459 return self.__class__(self.width, self.height)
461 def ToTuple(self) -> Tuple[Coordinate, Coordinate]:
462 """
463 Convert this 2D-size to a simple 2-element tuple.
465 :return: ``(width, height)`` tuple.
466 """
467 return self.width, self.height
469 def __repr__(self) -> str:
470 """
471 Returns the 2D size's string representation.
473 :returns: The string representation of the 2D size.
474 """
475 return f"Size2D({self.width}, {self.height})"
477 def __str__(self) -> str:
478 """
479 Returns the 2D size's string equivalent.
481 :returns: The string equivalent of the 2D size.
482 """
483 return f"({self.width}, {self.height})"
486@export
487class Segment2D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
488 """An implementation of a 2D cartesian segment."""
490 start: Point2D[Coordinate] #: Start point of a segment.
491 end: Point2D[Coordinate] #: End point of a segment.
493 def __init__(self, start: Point2D[Coordinate], end: Point2D[Coordinate], copyPoints: bool = True) -> None:
494 """
495 Initializes a 2-dimensional segment.
497 :param start: Start point of the segment.
498 :param end: End point of the segment.
499 :raises TypeError: If start/end is not of type Point2D.
500 """
501 if not isinstance(start, Point2D): 501 ↛ 502line 501 didn't jump to line 502 because the condition on line 501 was never true
502 ex = TypeError(f"Parameter 'start' is not of type Point2D.")
503 ex.add_note(f"Got type '{getFullyQualifiedName(start)}'.")
504 raise ex
505 if not isinstance(end, Point2D): 505 ↛ 506line 505 didn't jump to line 506 because the condition on line 505 was never true
506 ex = TypeError(f"Parameter 'end' is not of type Point2D.")
507 ex.add_note(f"Got type '{getFullyQualifiedName(end)}'.")
508 raise ex
510 self.start = start.Copy() if copyPoints else start
511 self.end = end.Copy() if copyPoints else end
514@export
515class LineSegment2D(Segment2D[Coordinate], Generic[Coordinate]):
516 """An implementation of a 2D cartesian line segment."""
518 @readonly
519 def Length(self) -> float:
520 """
521 Read-only property to return the Euclidean distance between start and end point.
523 :return: Euclidean distance between start and end point
524 """
525 return sqrt((self.end.x - self.start.x) ** 2 + (self.end.x - self.start.x) ** 2)
527 def AngleTo(self, other: "LineSegment2D[Coordinate]") -> float:
528 vectorA = self.ToOffset()
529 vectorB = other.ToOffset()
530 scalarProductAB = vectorA.xOffset * vectorB.xOffset + vectorA.yOffset * vectorB.yOffset
532 return acos(scalarProductAB / (abs(self.Length) * abs(other.Length)))
534 def ToOffset(self) -> Offset2D[Coordinate]:
535 """
536 Convert this 2D line segment to a 2D-offset.
538 :return: 2D-offset as :class:`Offset2D`
539 """
540 return self.end - self.start
542 def ToTuple(self) -> Tuple[Tuple[Coordinate, Coordinate], Tuple[Coordinate, Coordinate]]:
543 """
544 Convert this 2D line segment to a simple 2-element tuple of 2D-point tuples.
546 :return: ``((x1, y1), (x2, y2))`` tuple.
547 """
548 return self.start.ToTuple(), self.end.ToTuple()
550 def __repr__(self) -> str:
551 """
552 Returns the 2D line segment's string representation.
554 :returns: The string representation of the 2D line segment.
555 """
556 return f"LineSegment2D({self.start}, {self.end})"
558 def __str__(self) -> str:
559 """
560 Returns the 2D line segment's string equivalent.
562 :returns: The string equivalent of the 2D line segment.
563 """
564 return f"({self.start} → {self.end})"