Coverage for pyTooling/Cartesian2D/__init__.py: 93%
203 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-22 21:29 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-22 21:29 +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"""
32An implementation of 2D cartesian data structures for Python.
34.. seealso::
36 :mod:`pyTooling.Cartesian3D`
37 |rarr| The same data structures in three dimensions.
38 :mod:`pyTooling.Cartesian2D.Shapes`
39 |rarr| Shapes built from these points and offsets.
40"""
41from __future__ import annotations
43from math import sqrt, acos
44from typing import TypeVar, Union, Generic, Any, Self
46from pyTooling.Decorators import readonly, export
47from pyTooling.MetaClasses import ExtendedType
48from pyTooling.Common import getFullyQualifiedName
51Coordinate = TypeVar("Coordinate", bound=Union[int, float])
54@export
55class Point2D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
56 """An implementation of a 2D cartesian point."""
58 x: Coordinate #: The x-direction coordinate.
59 y: Coordinate #: The y-direction coordinate.
61 def __init__(self, x: Coordinate, y: Coordinate) -> None:
62 """
63 Initializes a 2-dimensional point.
65 :param x: X-coordinate.
66 :param y: Y-coordinate.
67 :raises TypeError: If x/y-coordinate is not of type integer or float.
68 """
69 if not isinstance(x, (int, float)):
70 ex = TypeError(f"Parameter 'x' is not of type integer or float.")
71 ex.add_note(f"Got type '{getFullyQualifiedName(x)}'.")
72 raise ex
73 if not isinstance(y, (int, float)):
74 ex = TypeError(f"Parameter 'y' is not of type integer or float.")
75 ex.add_note(f"Got type '{getFullyQualifiedName(y)}'.")
76 raise ex
78 self.x = x
79 self.y = y
81 def Copy(self) -> Self:
82 """
83 Create a new 2D-point as a copy of this 2D point.
85 :returns: Copy of this 2D-point.
87 .. seealso::
89 :meth:`+ operator <__add__>`
90 Create a new 2D-point moved by a positive 2D-offset.
91 :meth:`- operator <__sub__>`
92 Create a new 2D-point moved by a negative 2D-offset.
93 """
94 return self.__class__(self.x, self.y)
96 def ToTuple(self) -> tuple[Coordinate, Coordinate]:
97 """
98 Convert this 2D-Point to a simple 2-element tuple.
100 :returns: ``(x, y)`` tuple.
101 """
102 return self.x, self.y
104 def __add__(self, other: Any) -> Point2D[Coordinate]:
105 """
106 Adds a 2D-offset to this 2D-point and creates a new 2D-point.
108 :param other: A 2D-offset as :class:`Offset2D` or tuple.
109 :returns: A new 2D-point shifted by the 2D-offset.
110 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or tuple.
111 """
112 if isinstance(other, Offset2D):
113 return self.__class__(
114 self.x + other.xOffset,
115 self.y + other.yOffset
116 )
117 elif isinstance(other, tuple):
118 return self.__class__(
119 self.x + other[0],
120 self.y + other[1]
121 )
122 else:
123 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
124 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
125 raise ex
127 def __iadd__(self, other: Any) -> Self:
128 """
129 Adds a 2D-offset to this 2D-point (inplace).
131 :param other: A 2D-offset as :class:`Offset2D` or tuple.
132 :returns: This 2D-point.
133 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or tuple.
134 """
135 if isinstance(other, Offset2D):
136 self.x += other.xOffset
137 self.y += other.yOffset
138 elif isinstance(other, tuple):
139 self.x += other[0]
140 self.y += other[1]
141 else:
142 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
143 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
144 raise ex
146 return self
148 def __sub__(self, other: Any) -> Union[Offset2D[Coordinate], Point2D[Coordinate]]:
149 """
150 Subtract two 2D-Points from each other and create a new 2D-offset.
152 :param other: A 2D-point as :class:`Point2D`.
153 :returns: A new 2D-offset representing the distance between these two points.
154 :raises TypeError: If parameter 'other' is not a :class:`Point2D`.
155 """
156 if isinstance(other, Point2D):
157 return Offset2D(
158 self.x - other.x,
159 self.y - other.y
160 )
161 else:
162 ex = TypeError(f"Parameter 'other' is not of type Point2D.")
163 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
164 raise ex
166 def __isub__(self, other: Any) -> Self:
167 """
168 Subtracts a 2D-offset to this 2D-point (inplace).
170 :param other: A 2D-offset as :class:`Offset2D` or tuple.
171 :returns: This 2D-point.
172 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or tuple.
173 """
174 if isinstance(other, Offset2D):
175 self.x -= other.xOffset
176 self.y -= other.yOffset
177 elif isinstance(other, tuple):
178 self.x -= other[0]
179 self.y -= other[1]
180 else:
181 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
182 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
183 raise ex
185 return self
187 def __repr__(self) -> str:
188 """
189 Returns the 2D point's string representation.
191 :returns: The string representation of the 2D point.
192 """
193 return f"Point2D({self.x}, {self.y})"
195 def __str__(self) -> str:
196 """
197 Returns the 2D point's string equivalent.
199 :returns: The string equivalent of the 2D point.
200 """
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) -> Self:
215 """
216 An origin is a singular point, so it can't be copied.
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 2D origin's string representation.
226 :returns: The string representation of the 2D origin.
227 """
228 return f"Origin2D({self.x}, {self.y})"
231@export
232class Offset2D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
233 """An implementation of a 2D cartesian offset."""
235 xOffset: Coordinate #: The x-direction offset
236 yOffset: Coordinate #: The y-direction offset
238 def __init__(self, xOffset: Coordinate, yOffset: Coordinate) -> None:
239 """
240 Initializes a 2-dimensional offset.
242 :param xOffset: x-direction offset.
243 :param yOffset: y-direction offset.
244 :raises TypeError: If x/y-offset is not of type integer or float.
245 """
246 if not isinstance(xOffset, (int, float)):
247 ex = TypeError(f"Parameter 'xOffset' is not of type integer or float.")
248 ex.add_note(f"Got type '{getFullyQualifiedName(xOffset)}'.")
249 raise ex
250 if not isinstance(yOffset, (int, float)):
251 ex = TypeError(f"Parameter 'yOffset' is not of type integer or float.")
252 ex.add_note(f"Got type '{getFullyQualifiedName(yOffset)}'.")
253 raise ex
255 self.xOffset = xOffset
256 self.yOffset = yOffset
258 def Copy(self) -> Self:
259 """
260 Create a new 2D-offset as a copy of this 2D-offset.
262 :returns: Copy of this 2D-offset.
264 .. seealso::
266 :meth:`+ operator <__add__>`
267 Create a new 2D-offset moved by a positive 2D-offset.
268 :meth:`- operator <__sub__>`
269 Create a new 2D-offset moved by a negative 2D-offset.
270 """
271 return self.__class__(self.xOffset, self.yOffset)
273 def ToTuple(self) -> tuple[Coordinate, Coordinate]:
274 """
275 Convert this 2D-offset to a simple 2-element tuple.
277 :returns: ``(x, y)`` tuple.
278 """
279 return self.xOffset, self.yOffset
281 def __eq__(self, other: Any) -> bool:
282 """
283 Compare two 2D-offsets for equality.
285 :param other: Parameter to compare against.
286 :returns: ``True``, if both 2D-offsets are equal.
287 :raises TypeError: If parameter ``other`` is not of type :class:`Offset2D` or tuple.
288 """
289 if isinstance(other, Offset2D):
290 return self.xOffset == other.xOffset and self.yOffset == other.yOffset
291 elif isinstance(other, tuple):
292 return self.xOffset == other[0] and self.yOffset == other[1]
293 else:
294 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
295 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
296 raise ex
298 def __ne__(self, other: Any) -> bool:
299 """
300 Compare two 2D-offsets for inequality.
302 :param other: Parameter to compare against.
303 :returns: ``True``, if both 2D-offsets are unequal.
304 :raises TypeError: If parameter ``other`` is not of type :class:`Offset2D` or tuple.
305 """
306 return not self.__eq__(other)
308 def __neg__(self) -> Offset2D[Coordinate]:
309 """
310 Negate all components of this 2D-offset and create a new 2D-offset.
312 :returns: 2D-offset with negated offset components.
313 """
314 return self.__class__(
315 -self.xOffset,
316 -self.yOffset
317 )
319 def __add__(self, other: Any) -> Offset2D[Coordinate]:
320 """
321 Adds a 2D-offset to this 2D-offset and creates a new 2D-offset.
323 :param other: A 2D-offset as :class:`Offset2D` or tuple.
324 :returns: A new 2D-offset extended by the 2D-offset.
325 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or tuple.
326 """
327 if isinstance(other, Offset2D):
328 return self.__class__(
329 self.xOffset + other.xOffset,
330 self.yOffset + other.yOffset
331 )
332 elif isinstance(other, tuple):
333 return self.__class__(
334 self.xOffset + other[0],
335 self.yOffset + other[1]
336 )
337 else:
338 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
339 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
340 raise ex
342 def __iadd__(self, other: Any) -> Self:
343 """
344 Adds a 2D-offset to this 2D-offset (inplace).
346 :param other: A 2D-offset as :class:`Offset2D` or tuple.
347 :returns: This 2D-point.
348 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or tuple.
349 """
350 if isinstance(other, Offset2D):
351 self.xOffset += other.xOffset
352 self.yOffset += other.yOffset
353 elif isinstance(other, tuple):
354 self.xOffset += other[0]
355 self.yOffset += other[1]
356 else:
357 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
358 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
359 raise ex
361 return self
363 def __sub__(self, other: Any) -> Offset2D[Coordinate]:
364 """
365 Subtracts a 2D-offset from this 2D-offset and creates a new 2D-offset.
367 :param other: A 2D-offset as :class:`Offset2D` or tuple.
368 :returns: A new 2D-offset reduced by the 2D-offset.
369 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or tuple.
370 """
371 if isinstance(other, Offset2D):
372 return self.__class__(
373 self.xOffset - other.xOffset,
374 self.yOffset - other.yOffset
375 )
376 elif isinstance(other, tuple):
377 return self.__class__(
378 self.xOffset - other[0],
379 self.yOffset - other[1]
380 )
381 else:
382 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
383 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
384 raise ex
386 def __isub__(self, other: Any) -> Self:
387 """
388 Subtracts a 2D-offset from this 2D-offset (inplace).
390 :param other: A 2D-offset as :class:`Offset2D` or tuple.
391 :returns: This 2D-point.
392 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or tuple.
393 """
394 if isinstance(other, Offset2D):
395 self.xOffset -= other.xOffset
396 self.yOffset -= other.yOffset
397 elif isinstance(other, tuple):
398 self.xOffset -= other[0]
399 self.yOffset -= other[1]
400 else:
401 ex = TypeError(f"Parameter 'other' is not of type Offset2D or tuple.")
402 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
403 raise ex
405 return self
407 def __repr__(self) -> str:
408 """
409 Returns the 2D offset's string representation.
411 :returns: The string representation of the 2D offset.
412 """
413 return f"Offset2D({self.xOffset}, {self.yOffset})"
415 def __str__(self) -> str:
416 """
417 Returns the 2D offset's string equivalent.
419 :returns: The string equivalent of the 2D offset.
420 """
421 return f"({self.xOffset}, {self.yOffset})"
424@export
425class Size2D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
426 """An implementation of a 2D cartesian size."""
428 width: Coordinate #: width in x-direction.
429 height: Coordinate #: height in y-direction.
431 def __init__(self, width: Coordinate, height: Coordinate) -> None:
432 """
433 Initializes a 2-dimensional size.
435 :param width: width in x-direction.
436 :param height: height in y-direction.
437 :raises TypeError: If width/height is not of type integer or float.
438 """
439 if not isinstance(width, (int, float)):
440 ex = TypeError(f"Parameter 'width' is not of type integer or float.")
441 ex.add_note(f"Got type '{getFullyQualifiedName(width)}'.")
442 raise ex
443 if not isinstance(height, (int, float)):
444 ex = TypeError(f"Parameter 'height' is not of type integer or float.")
445 ex.add_note(f"Got type '{getFullyQualifiedName(height)}'.")
446 raise ex
448 self.width = width
449 self.height = height
451 def Copy(self) -> Self:
452 """
453 Create a new 2D-size as a copy of this 2D-size.
455 :returns: Copy of this 2D-size.
456 """
457 return self.__class__(self.width, self.height)
459 def ToTuple(self) -> tuple[Coordinate, Coordinate]:
460 """
461 Convert this 2D-size to a simple 2-element tuple.
463 :returns: ``(width, height)`` tuple.
464 """
465 return self.width, self.height
467 def __repr__(self) -> str:
468 """
469 Returns the 2D size's string representation.
471 :returns: The string representation of the 2D size.
472 """
473 return f"Size2D({self.width}, {self.height})"
475 def __str__(self) -> str:
476 """
477 Returns the 2D size's string equivalent.
479 :returns: The string equivalent of the 2D size.
480 """
481 return f"({self.width}, {self.height})"
484@export
485class Segment2D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
486 """An implementation of a 2D cartesian segment."""
488 start: Point2D[Coordinate] #: Start point of a segment.
489 end: Point2D[Coordinate] #: End point of a segment.
491 def __init__(self, start: Point2D[Coordinate], end: Point2D[Coordinate], copyPoints: bool = True) -> None:
492 """
493 Initializes a 2-dimensional segment.
495 :param start: Start point of the segment.
496 :param end: End point of the segment.
497 :param copyPoints: Optional, if ``True``, the given points are copied instead of referenced.
498 :raises TypeError: If start/end is not of type :class:`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 :returns: 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 """
528 Compute the angle between this line segment and another one.
530 :param other: The second line segment.
531 :returns: The angle in radians.
532 """
533 vectorA = self.ToOffset()
534 vectorB = other.ToOffset()
535 scalarProductAB = vectorA.xOffset * vectorB.xOffset + vectorA.yOffset * vectorB.yOffset
537 return acos(scalarProductAB / (abs(self.Length) * abs(other.Length)))
539 def ToOffset(self) -> Offset2D[Coordinate]:
540 """
541 Convert this 2D line segment to a 2D-offset.
543 :returns: 2D-offset as :class:`Offset2D`
544 """
545 return self.end - self.start
547 def ToTuple(self) -> tuple[tuple[Coordinate, Coordinate], tuple[Coordinate, Coordinate]]:
548 """
549 Convert this 2D line segment to a simple 2-element tuple of 2D-point tuples.
551 :returns: ``((x1, y1), (x2, y2))`` tuple.
552 """
553 return self.start.ToTuple(), self.end.ToTuple()
555 def __repr__(self) -> str:
556 """
557 Returns the 2D line segment's string representation.
559 :returns: The string representation of the 2D line segment.
560 """
561 return f"LineSegment2D({self.start}, {self.end})"
563 def __str__(self) -> str:
564 """
565 Returns the 2D line segment's string equivalent.
567 :returns: The string equivalent of the 2D line segment.
568 """
569 return f"({self.start} → {self.end})"