Coverage for pyTooling/Cartesian2D/__init__.py: 93%
203 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-03 23:02 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-03 23:02 +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("Parameter 'x' is not of type integer or float.")
71 ex.add_note(f"Got type '{getFullyQualifiedName(x)}'.")
72 raise ex
74 if not isinstance(y, (int, float)):
75 ex = TypeError("Parameter 'y' is not of type integer or float.")
76 ex.add_note(f"Got type '{getFullyQualifiedName(y)}'.")
77 raise ex
79 self.x = x
80 self.y = y
82 def Copy(self) -> Self:
83 """
84 Create a new 2D-point as a copy of this 2D point.
86 :returns: Copy of this 2D-point.
88 .. seealso::
90 :meth:`+ operator <__add__>`
91 Create a new 2D-point moved by a positive 2D-offset.
92 :meth:`- operator <__sub__>`
93 Create a new 2D-point moved by a negative 2D-offset.
94 """
95 return self.__class__(self.x, self.y)
97 def ToTuple(self) -> tuple[Coordinate, Coordinate]:
98 """
99 Convert this 2D-Point to a simple 2-element tuple.
101 :returns: ``(x, y)`` tuple.
102 """
103 return self.x, self.y
105 def __add__(self, other: Any) -> Point2D[Coordinate]:
106 """
107 Adds a 2D-offset to this 2D-point and creates a new 2D-point.
109 :param other: A 2D-offset as :class:`Offset2D` or tuple.
110 :returns: A new 2D-point shifted by the 2D-offset.
111 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or tuple.
112 """
113 if isinstance(other, Offset2D):
114 return self.__class__(
115 self.x + other.xOffset,
116 self.y + other.yOffset
117 )
118 elif isinstance(other, tuple):
119 return self.__class__(
120 self.x + other[0],
121 self.y + other[1]
122 )
123 else:
124 ex = TypeError("Parameter 'other' is not of type Offset2D or tuple.")
125 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
126 raise ex
128 def __iadd__(self, other: Any) -> Self:
129 """
130 Adds a 2D-offset to this 2D-point (inplace).
132 :param other: A 2D-offset as :class:`Offset2D` or tuple.
133 :returns: This 2D-point.
134 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or tuple.
135 """
136 if isinstance(other, Offset2D):
137 self.x += other.xOffset
138 self.y += other.yOffset
139 elif isinstance(other, tuple):
140 self.x += other[0]
141 self.y += other[1]
142 else:
143 ex = TypeError("Parameter 'other' is not of type Offset2D or tuple.")
144 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
145 raise ex
147 return self
149 def __sub__(self, other: Any) -> Union[Offset2D[Coordinate], Point2D[Coordinate]]:
150 """
151 Subtract two 2D-Points from each other and create a new 2D-offset.
153 :param other: A 2D-point as :class:`Point2D`.
154 :returns: A new 2D-offset representing the distance between these two points.
155 :raises TypeError: If parameter 'other' is not a :class:`Point2D`.
156 """
157 if isinstance(other, Point2D):
158 return Offset2D(
159 self.x - other.x,
160 self.y - other.y
161 )
162 else:
163 ex = TypeError("Parameter 'other' is not of type Point2D.")
164 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
165 raise ex
167 def __isub__(self, other: Any) -> Self:
168 """
169 Subtracts a 2D-offset to this 2D-point (inplace).
171 :param other: A 2D-offset as :class:`Offset2D` or tuple.
172 :returns: This 2D-point.
173 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or tuple.
174 """
175 if isinstance(other, Offset2D):
176 self.x -= other.xOffset
177 self.y -= other.yOffset
178 elif isinstance(other, tuple):
179 self.x -= other[0]
180 self.y -= other[1]
181 else:
182 ex = TypeError("Parameter 'other' is not of type Offset2D or tuple.")
183 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
184 raise ex
186 return self
188 def __repr__(self) -> str:
189 """
190 Returns the 2D point's string representation.
192 :returns: The string representation of the 2D point.
193 """
194 return f"Point2D({self.x}, {self.y})"
196 def __str__(self) -> str:
197 """
198 Returns the 2D point's string equivalent.
200 :returns: The string equivalent of the 2D point.
201 """
202 return f"({self.x}, {self.y})"
205@export
206class Origin2D(Point2D[Coordinate], Generic[Coordinate]):
207 """An implementation of a 2D cartesian origin."""
209 def __init__(self) -> None:
210 """
211 Initializes a 2-dimensional origin.
212 """
213 super().__init__(0, 0)
215 def Copy(self) -> Self:
216 """
217 An origin is a singular point, so it can't be copied.
219 :raises RuntimeError: Because an origin can't be copied.
220 """
221 raise RuntimeError("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("Parameter 'xOffset' is not of type integer or float.")
249 ex.add_note(f"Got type '{getFullyQualifiedName(xOffset)}'.")
250 raise ex
252 if not isinstance(yOffset, (int, float)):
253 ex = TypeError("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) -> 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: Any) -> 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 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("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: Any) -> 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 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 tuple.
326 :returns: A new 2D-offset extended by the 2D-offset.
327 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or 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("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) -> Self:
345 """
346 Adds a 2D-offset to this 2D-offset (inplace).
348 :param other: A 2D-offset as :class:`Offset2D` or tuple.
349 :returns: This 2D-point.
350 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or 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("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 tuple.
370 :returns: A new 2D-offset reduced by the 2D-offset.
371 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or 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("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) -> Self:
389 """
390 Subtracts a 2D-offset from this 2D-offset (inplace).
392 :param other: A 2D-offset as :class:`Offset2D` or tuple.
393 :returns: This 2D-point.
394 :raises TypeError: If parameter 'other' is not a :class:`Offset2D` or 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("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("Parameter 'width' is not of type integer or float.")
443 ex.add_note(f"Got type '{getFullyQualifiedName(width)}'.")
444 raise ex
446 if not isinstance(height, (int, float)):
447 ex = TypeError("Parameter 'height' is not of type integer or float.")
448 ex.add_note(f"Got type '{getFullyQualifiedName(height)}'.")
449 raise ex
451 self.width = width
452 self.height = height
454 def Copy(self) -> Self:
455 """
456 Create a new 2D-size as a copy of this 2D-size.
458 :returns: Copy of this 2D-size.
459 """
460 return self.__class__(self.width, self.height)
462 def ToTuple(self) -> tuple[Coordinate, Coordinate]:
463 """
464 Convert this 2D-size to a simple 2-element tuple.
466 :returns: ``(width, height)`` tuple.
467 """
468 return self.width, self.height
470 def __repr__(self) -> str:
471 """
472 Returns the 2D size's string representation.
474 :returns: The string representation of the 2D size.
475 """
476 return f"Size2D({self.width}, {self.height})"
478 def __str__(self) -> str:
479 """
480 Returns the 2D size's string equivalent.
482 :returns: The string equivalent of the 2D size.
483 """
484 return f"({self.width}, {self.height})"
487@export
488class Segment2D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
489 """An implementation of a 2D cartesian segment."""
491 start: Point2D[Coordinate] #: Start point of a segment.
492 end: Point2D[Coordinate] #: End point of a segment.
494 def __init__(self, start: Point2D[Coordinate], end: Point2D[Coordinate], copyPoints: bool = True) -> None:
495 """
496 Initializes a 2-dimensional segment.
498 :param start: Start point of the segment.
499 :param end: End point of the segment.
500 :param copyPoints: Optional, if ``True``, the given points are copied instead of referenced.
501 :raises TypeError: If start/end is not of type :class:`Point2D`.
502 """
503 if not isinstance(start, Point2D): 503 ↛ 504line 503 didn't jump to line 504 because the condition on line 503 was never true
504 ex = TypeError("Parameter 'start' is not of type Point2D.")
505 ex.add_note(f"Got type '{getFullyQualifiedName(start)}'.")
506 raise ex
508 if not isinstance(end, Point2D): 508 ↛ 509line 508 didn't jump to line 509 because the condition on line 508 was never true
509 ex = TypeError("Parameter 'end' is not of type Point2D.")
510 ex.add_note(f"Got type '{getFullyQualifiedName(end)}'.")
511 raise ex
513 self.start = start.Copy() if copyPoints else start
514 self.end = end.Copy() if copyPoints else end
517@export
518class LineSegment2D(Segment2D[Coordinate], Generic[Coordinate]):
519 """An implementation of a 2D cartesian line segment."""
521 @readonly
522 def Length(self) -> float:
523 """
524 Read-only property to return the Euclidean distance between start and end point.
526 :returns: Euclidean distance between start and end point
527 """
528 return sqrt((self.end.x - self.start.x) ** 2 + (self.end.x - self.start.x) ** 2)
530 def AngleTo(self, other: LineSegment2D[Coordinate]) -> float:
531 """
532 Compute the angle between this line segment and another one.
534 :param other: The second line segment.
535 :returns: The angle in radians.
536 """
537 vectorA = self.ToOffset()
538 vectorB = other.ToOffset()
539 scalarProductAB = vectorA.xOffset * vectorB.xOffset + vectorA.yOffset * vectorB.yOffset
541 return acos(scalarProductAB / (abs(self.Length) * abs(other.Length)))
543 def ToOffset(self) -> Offset2D[Coordinate]:
544 """
545 Convert this 2D line segment to a 2D-offset.
547 :returns: 2D-offset as :class:`Offset2D`
548 """
549 return self.end - self.start
551 def ToTuple(self) -> tuple[tuple[Coordinate, Coordinate], tuple[Coordinate, Coordinate]]:
552 """
553 Convert this 2D line segment to a simple 2-element tuple of 2D-point tuples.
555 :returns: ``((x1, y1), (x2, y2))`` tuple.
556 """
557 return self.start.ToTuple(), self.end.ToTuple()
559 def __repr__(self) -> str:
560 """
561 Returns the 2D line segment's string representation.
563 :returns: The string representation of the 2D line segment.
564 """
565 return f"LineSegment2D({self.start}, {self.end})"
567 def __str__(self) -> str:
568 """
569 Returns the 2D line segment's string equivalent.
571 :returns: The string equivalent of the 2D line segment.
572 """
573 return f"({self.start} → {self.end})"