Coverage for pyTooling/Cartesian2D/__init__.py: 94%
188 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-25 22:22 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-25 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."""
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 if version_info >= (3, 11): # pragma: no cover
76 ex.add_note(f"Got type '{getFullyQualifiedName(x)}'.")
77 raise ex
78 if not isinstance(y, (int, float)):
79 ex = TypeError(f"Parameter 'y' is not of type integer or float.")
80 if version_info >= (3, 11): # pragma: no cover
81 ex.add_note(f"Got type '{getFullyQualifiedName(y)}'.")
82 raise ex
84 self.x = x
85 self.y = y
87 def Copy(self) -> "Point2D[Coordinate]": # TODO: Python 3.11: -> Self:
88 """
89 Create a new 2D-point as a copy of this 2D point.
91 :returns: Copy of this 2D-point.
93 .. seealso::
95 :meth:`+ operator <__add__>`
96 Create a new 2D-point moved by a positive 2D-offset.
97 :meth:`- operator <__sub__>`
98 Create a new 2D-point moved by a negative 2D-offset.
99 """
100 return self.__class__(self.x, self.y)
102 def ToTuple(self) -> Tuple[Coordinate, Coordinate]:
103 """
104 Convert this 2D-Point to a simple 2-element tuple.
106 :returns: ``(x, y)`` tuple.
107 """
108 return self.x, self.y
110 def __add__(self, other: Any) -> "Point2D[Coordinate]":
111 """
112 Adds a 2D-offset to this 2D-point and creates a new 2D-point.
114 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`.
115 :returns: A new 2D-point shifted by the 2D-offset.
116 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`.
117 """
118 if isinstance(other, Offset2D):
119 return self.__class__(
120 self.x + other.xOffset,
121 self.y + other.yOffset
122 )
123 elif isinstance(other, tuple):
124 return self.__class__(
125 self.x + other[0],
126 self.y + other[1]
127 )
128 else:
129 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
130 if version_info >= (3, 11): # pragma: no cover
131 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
132 raise ex
134 def __iadd__(self, other: Any) -> "Point2D[Coordinate]": # TODO: Python 3.11: -> Self:
135 """
136 Adds a 2D-offset to this 2D-point (inplace).
138 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`.
139 :returns: This 2D-point.
140 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`.
141 """
142 if isinstance(other, Offset2D):
143 self.x += other.xOffset
144 self.y += other.yOffset
145 elif isinstance(other, tuple):
146 self.x += other[0]
147 self.y += other[1]
148 else:
149 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
150 if version_info >= (3, 11): # pragma: no cover
151 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
152 raise ex
154 return self
156 def __sub__(self, other: Any) -> Union["Offset2D[Coordinate]", "Point2D[Coordinate]"]:
157 """
158 Subtract two 2D-Points from each other and create a new 2D-offset.
160 :param other: A 2D-point as :class:`Point2D`.
161 :returns: A new 2D-offset representing the distance between these two points.
162 :raises TypeError: If parameter 'other' is not a :class:`Point2D`.
163 """
164 if isinstance(other, Point2D):
165 return Offset2D(
166 self.x - other.x,
167 self.y - other.y
168 )
169 else:
170 ex = TypeError(f"Parameter 'other' is not of type Point2D.")
171 if version_info >= (3, 11): # pragma: no cover
172 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
173 raise ex
175 def __isub__(self, other: Any) -> "Point2D[Coordinate]": # TODO: Python 3.11: -> Self:
176 """
177 Subtracts a 2D-offset to this 2D-point (inplace).
179 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`.
180 :returns: This 2D-point.
181 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`.
182 """
183 if isinstance(other, Offset2D):
184 self.x -= other.xOffset
185 self.y -= other.yOffset
186 elif isinstance(other, tuple):
187 self.x -= other[0]
188 self.y -= other[1]
189 else:
190 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
191 if version_info >= (3, 11): # pragma: no cover
192 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
193 raise ex
195 return self
197 def __repr__(self) -> str:
198 return f"Point2D({self.x}, {self.y})"
200 def __str__(self) -> str:
201 return f"({self.x}, {self.y})"
204@export
205class Origin2D(Point2D[Coordinate], Generic[Coordinate]):
206 """An implementation of a 2D cartesian origin."""
208 def __init__(self) -> None:
209 """
210 Initializes a 2-dimensional origin.
211 """
212 super().__init__(0, 0)
214 def Copy(self) -> "Origin2D[Coordinate]": # TODO: Python 3.11: -> Self:
215 """
216 :raises RuntimeError: Because an origin can't be copied.
217 """
218 raise RuntimeError(f"An origin can't be copied.")
220 def __repr__(self) -> str:
221 return f"Origin2D({self.x}, {self.y})"
224@export
225class Offset2D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
226 """An implementation of a 2D cartesian offset."""
228 xOffset: Coordinate #: The x-direction offset
229 yOffset: Coordinate #: The y-direction offset
231 def __init__(self, xOffset: Coordinate, yOffset: Coordinate) -> None:
232 """
233 Initializes a 2-dimensional offset.
235 :param xOffset: x-direction offset.
236 :param yOffset: y-direction offset.
237 :raises TypeError: If x/y-offset is not of type integer or float.
238 """
239 if not isinstance(xOffset, (int, float)):
240 ex = TypeError(f"Parameter 'xOffset' is not of type integer or float.")
241 if version_info >= (3, 11): # pragma: no cover
242 ex.add_note(f"Got type '{getFullyQualifiedName(xOffset)}'.")
243 raise ex
244 if not isinstance(yOffset, (int, float)):
245 ex = TypeError(f"Parameter 'yOffset' is not of type integer or float.")
246 if version_info >= (3, 11): # pragma: no cover
247 ex.add_note(f"Got type '{getFullyQualifiedName(yOffset)}'.")
248 raise ex
250 self.xOffset = xOffset
251 self.yOffset = yOffset
253 def Copy(self) -> "Offset2D[Coordinate]": # TODO: Python 3.11: -> Self:
254 """
255 Create a new 2D-offset as a copy of this 2D-offset.
257 :returns: Copy of this 2D-offset.
259 .. seealso::
261 :meth:`+ operator <__add__>`
262 Create a new 2D-offset moved by a positive 2D-offset.
263 :meth:`- operator <__sub__>`
264 Create a new 2D-offset moved by a negative 2D-offset.
265 """
266 return self.__class__(self.xOffset, self.yOffset)
268 def ToTuple(self) -> Tuple[Coordinate, Coordinate]:
269 """
270 Convert this 2D-offset to a simple 2-element tuple.
272 :returns: ``(x, y)`` tuple.
273 """
274 return self.xOffset, self.yOffset
276 def __eq__(self, other) -> bool:
277 """
278 Compare two 2D-offsets for equality.
280 :param other: Parameter to compare against.
281 :returns: ``True``, if both 2D-offsets are equal.
282 :raises TypeError: If parameter ``other`` is not of type :class:`Offset2D` or :class:`tuple`.
283 """
284 if isinstance(other, Offset2D):
285 return self.xOffset == other.xOffset and self.yOffset == other.yOffset
286 elif isinstance(other, tuple):
287 return self.xOffset == other[0] and self.yOffset == other[1]
288 else:
289 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
290 if version_info >= (3, 11): # pragma: no cover
291 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
292 raise ex
294 def __ne__(self, other) -> bool:
295 """
296 Compare two 2D-offsets for inequality.
298 :param other: Parameter to compare against.
299 :returns: ``True``, if both 2D-offsets are unequal.
300 :raises TypeError: If parameter ``other`` is not of type :class:`Offset2D` or :class:`tuple`.
301 """
302 return not self.__eq__(other)
304 def __neg__(self) -> "Offset2D[Coordinate]":
305 """
306 Negate all components of this 2D-offset and create a new 2D-offset.
308 :returns: 2D-offset with negated offset components.
309 """
310 return self.__class__(
311 -self.xOffset,
312 -self.yOffset
313 )
315 def __add__(self, other: Any) -> "Offset2D[Coordinate]":
316 """
317 Adds a 2D-offset to this 2D-offset and creates a new 2D-offset.
319 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`.
320 :returns: A new 2D-offset extended by the 2D-offset.
321 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`.
322 """
323 if isinstance(other, Offset2D):
324 return self.__class__(
325 self.xOffset + other.xOffset,
326 self.yOffset + other.yOffset
327 )
328 elif isinstance(other, tuple):
329 return self.__class__(
330 self.xOffset + other[0],
331 self.yOffset + other[1]
332 )
333 else:
334 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
335 if version_info >= (3, 11): # pragma: no cover
336 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
337 raise ex
339 def __iadd__(self, other: Any) -> "Offset2D[Coordinate]": # TODO: Python 3.11: -> Self:
340 """
341 Adds a 2D-offset to this 2D-offset (inplace).
343 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`.
344 :returns: This 2D-point.
345 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`.
346 """
347 if isinstance(other, Offset2D):
348 self.xOffset += other.xOffset
349 self.yOffset += other.yOffset
350 elif isinstance(other, tuple):
351 self.xOffset += other[0]
352 self.yOffset += other[1]
353 else:
354 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
355 if version_info >= (3, 11): # pragma: no cover
356 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
357 raise ex
359 return self
361 def __sub__(self, other: Any) -> "Offset2D[Coordinate]":
362 """
363 Subtracts a 2D-offset from this 2D-offset and creates a new 2D-offset.
365 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`.
366 :returns: A new 2D-offset reduced by the 2D-offset.
367 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`.
368 """
369 if isinstance(other, Offset2D):
370 return self.__class__(
371 self.xOffset - other.xOffset,
372 self.yOffset - other.yOffset
373 )
374 elif isinstance(other, tuple):
375 return self.__class__(
376 self.xOffset - other[0],
377 self.yOffset - other[1]
378 )
379 else:
380 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
381 if version_info >= (3, 11): # pragma: no cover
382 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
383 raise ex
385 def __isub__(self, other: Any) -> "Offset2D[Coordinate]": # TODO: Python 3.11: -> Self:
386 """
387 Subtracts a 2D-offset from this 2D-offset (inplace).
389 :param other: A 2D-offset as :class:`Offset2D` or :class:`tuple`.
390 :returns: This 2D-point.
391 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or :class:`tuple`.
392 """
393 if isinstance(other, Offset2D):
394 self.xOffset -= other.xOffset
395 self.yOffset -= other.yOffset
396 elif isinstance(other, tuple):
397 self.xOffset -= other[0]
398 self.yOffset -= other[1]
399 else:
400 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
401 if version_info >= (3, 11): # pragma: no cover
402 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
403 raise ex
405 return self
407 def __repr__(self) -> str:
408 return f"Offset2D({self.xOffset}, {self.yOffset})"
410 def __str__(self) -> str:
411 return f"({self.xOffset}, {self.yOffset})"
414@export
415class Size2D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
416 """An implementation of a 2D cartesian size."""
418 width: Coordinate #: width in x-direction.
419 height: Coordinate #: height in y-direction.
421 def __init__(self, width: Coordinate, height: Coordinate) -> None:
422 """
423 Initializes a 2-dimensional size.
425 :param width: width in x-direction.
426 :param height: height in y-direction.
427 :raises TypeError: If width/height is not of type integer or float.
428 """
429 if not isinstance(width, (int, float)):
430 ex = TypeError(f"Parameter 'width' is not of type integer or float.")
431 if version_info >= (3, 11): # pragma: no cover
432 ex.add_note(f"Got type '{getFullyQualifiedName(width)}'.")
433 raise ex
434 if not isinstance(height, (int, float)):
435 ex = TypeError(f"Parameter 'height' is not of type integer or float.")
436 if version_info >= (3, 11): # pragma: no cover
437 ex.add_note(f"Got type '{getFullyQualifiedName(height)}'.")
438 raise ex
440 self.width = width
441 self.height = height
443 def Copy(self) -> "Size2D": # TODO: Python 3.11: -> Self:
444 """
445 Create a new 2D-size as a copy of this 2D-size.
447 :returns: Copy of this 2D-size.
448 """
449 return self.__class__(self.width, self.height)
451 def ToTuple(self) -> Tuple[Coordinate, Coordinate]:
452 """
453 Convert this 2D-size to a simple 2-element tuple.
455 :return: ``(width, height)`` tuple.
456 """
457 return self.width, self.height
459 def __repr__(self) -> str:
460 return f"Size2D({self.width}, {self.height})"
462 def __str__(self) -> str:
463 return f"({self.width}, {self.height})"
466@export
467class Segment2D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
468 """An implementation of a 2D cartesian segment."""
470 start: Point2D[Coordinate] #: Start point of a segment.
471 end: Point2D[Coordinate] #: End point of a segment.
473 def __init__(self, start: Point2D[Coordinate], end: Point2D[Coordinate], copyPoints: bool = True) -> None:
474 """
475 Initializes a 2-dimensional segment.
477 :param start: Start point of the segment.
478 :param end: End point of the segment.
479 :raises TypeError: If start/end is not of type Point2D.
480 """
481 if not isinstance(start, Point2D): 481 ↛ 482line 481 didn't jump to line 482 because the condition on line 481 was never true
482 ex = TypeError(f"Parameter 'start' is not of type Point2D.")
483 if version_info >= (3, 11): # pragma: no cover
484 ex.add_note(f"Got type '{getFullyQualifiedName(start)}'.")
485 raise ex
486 if not isinstance(end, Point2D): 486 ↛ 487line 486 didn't jump to line 487 because the condition on line 486 was never true
487 ex = TypeError(f"Parameter 'end' is not of type Point2D.")
488 if version_info >= (3, 11): # pragma: no cover
489 ex.add_note(f"Got type '{getFullyQualifiedName(end)}'.")
490 raise ex
492 self.start = start.Copy() if copyPoints else start
493 self.end = end.Copy() if copyPoints else end
496@export
497class LineSegment2D(Segment2D[Coordinate], Generic[Coordinate]):
498 """An implementation of a 2D cartesian line segment."""
500 @readonly
501 def Length(self) -> float:
502 """
503 Read-only property to return the Euclidean distance between start and end point.
505 :return: Euclidean distance between start and end point
506 """
507 return sqrt((self.end.x - self.start.x) ** 2 + (self.end.x - self.start.x) ** 2)
509 def AngleTo(self, other: "LineSegment2D[Coordinate]") -> float:
510 vectorA = self.ToOffset()
511 vectorB = other.ToOffset()
512 scalarProductAB = vectorA.xOffset * vectorB.xOffset + vectorA.yOffset * vectorB.yOffset
514 return acos(scalarProductAB / (abs(self.Length) * abs(other.Length)))
516 def ToOffset(self) -> Offset2D[Coordinate]:
517 """
518 Convert this 2D line segment to a 2D-offset.
520 :return: 2D-offset as :class:`Offset2D`
521 """
522 return self.end - self.start
524 def ToTuple(self) -> Tuple[Tuple[Coordinate, Coordinate], Tuple[Coordinate, Coordinate]]:
525 """
526 Convert this 2D line segment to a simple 2-element tuple of 2D-point tuples.
528 :return: ``((x1, y1), (x2, y2))`` tuple.
529 """
530 return self.start.ToTuple(), self.end.ToTuple()
532 def __repr__(self) -> str:
533 return f"LineSegment2D({self.start}, {self.end})"
535 def __str__(self) -> str:
536 return f"({self.start} → {self.end})"