ZLayout EDA Library v1.0.0
Advanced Electronic Design Automation Layout Library with Bilingual Documentation
Loading...
Searching...
No Matches
geometry.py
Go to the documentation of this file.
1"""
2Basic geometric data structures for layout processing.
3"""
4
5import math
6from typing import List, Tuple, Optional, Union
7
8# Optional numpy import - fallback to built-in types if not available
9try:
10 import numpy as np
11 HAS_NUMPY = True
12except ImportError:
13 HAS_NUMPY = False
14
15
16class Point:
17 """2D point with coordinates and utility methods."""
18
19 def __init__(self, x: float, y: float):
20 self.x = float(x)
21 self.y = float(y)
22
23 def __repr__(self) -> str:
24 # Show integers as integers, floats as floats
25 x_str = str(int(self.x)) if self.x == int(self.x) else str(self.x)
26 y_str = str(int(self.y)) if self.y == int(self.y) else str(self.y)
27 return f"Point({x_str}, {y_str})"
28
29 def __eq__(self, other) -> bool:
30 if not isinstance(other, Point):
31 return False
32 return abs(self.x - other.x) < 1e-10 and abs(self.y - other.y) < 1e-10
33
34 def __hash__(self) -> int:
35 return hash((round(self.x, 10), round(self.y, 10)))
36
37 def distance_to(self, other: 'Point') -> float:
38 """Calculate Euclidean distance to another point."""
39 return math.sqrt((self.x - other.x)**2 + (self.y - other.y)**2)
40
41 def distance_to_line(self, line_start: 'Point', line_end: 'Point') -> float:
42 """Calculate distance from this point to a line segment."""
43 # Vector from line_start to line_end
44 line_vec = Point(line_end.x - line_start.x, line_end.y - line_start.y)
45 line_length_sq = line_vec.x**2 + line_vec.y**2
46
47 if line_length_sq < 1e-10: # Degenerate line
48 return self.distance_to(line_start)
49
50 # Vector from line_start to this point
51 point_vec = Point(self.x - line_start.x, self.y - line_start.y)
52
53 # Project point onto line
54 t = max(0, min(1, (point_vec.x * line_vec.x + point_vec.y * line_vec.y) / line_length_sq))
55
56 # Find closest point on line segment
57 closest = Point(line_start.x + t * line_vec.x, line_start.y + t * line_vec.y)
58
59 return self.distance_to(closest)
60
61 def to_numpy(self):
62 """Convert to numpy array (if numpy is available)."""
63 if HAS_NUMPY:
64 return np.array([self.x, self.y])
65 else:
66 return [self.x, self.y]
67
68
69class Rectangle:
70 """Axis-aligned rectangle for bounding boxes and simple components."""
71
72 def __init__(self, x: float, y: float, width: float, height: float):
73 self.x = float(x)
74 self.y = float(y)
75 self.width = float(width)
76 self.height = float(height)
77
78 def __repr__(self) -> str:
79 return f"Rectangle({self.x}, {self.y}, {self.width}, {self.height})"
80
81 @property
82 def left(self) -> float:
83 return self.x
84
85 @property
86 def right(self) -> float:
87 return self.x + self.width
88
89 @property
90 def top(self) -> float:
91 return self.y + self.height
92
93 @property
94 def bottom(self) -> float:
95 return self.y
96
97 @property
98 def center(self) -> Point:
99 return Point(self.x + self.width / 2, self.y + self.height / 2)
100
101 def contains_point(self, point: Point) -> bool:
102 """Check if point is inside rectangle."""
103 return (self.left <= point.x <= self.right and
104 self.bottom <= point.y <= self.top)
105
106 def intersects(self, other: 'Rectangle') -> bool:
107 """Check if this rectangle intersects with another."""
108 return not (self.right < other.left or other.right < self.left or
109 self.top < other.bottom or other.top < self.bottom)
110
111 def to_polygon(self) -> 'Polygon':
112 """Convert rectangle to polygon."""
113 vertices = [
114 Point(self.left, self.bottom),
115 Point(self.right, self.bottom),
116 Point(self.right, self.top),
117 Point(self.left, self.top)
118 ]
119 return Polygon(vertices)
120
121
122class Polygon:
123 """Polygon class supporting both convex and concave polygons."""
124
125 def __init__(self, vertices: List[Point]):
126 if len(vertices) < 3:
127 raise ValueError("Polygon must have at least 3 vertices")
128 self.vertices = vertices.copy()
129
130 def __repr__(self) -> str:
131 return f"Polygon({len(self.vertices)} vertices)"
132
133 @property
134 def edges(self) -> List[Tuple[Point, Point]]:
135 """Get all edges as (start, end) point pairs."""
136 edges = []
137 n = len(self.vertices)
138 for i in range(n):
139 edges.append((self.vertices[i], self.vertices[(i + 1) % n]))
140 return edges
141
142 def bounding_box(self) -> Rectangle:
143 """Calculate axis-aligned bounding box."""
144 if not self.vertices:
145 raise ValueError("Cannot calculate bounding box of empty polygon")
146
147 min_x = min(v.x for v in self.vertices)
148 max_x = max(v.x for v in self.vertices)
149 min_y = min(v.y for v in self.vertices)
150 max_y = max(v.y for v in self.vertices)
151
152 return Rectangle(min_x, min_y, max_x - min_x, max_y - min_y)
153
154 def area(self) -> float:
155 """Calculate polygon area using shoelace formula."""
156 if len(self.vertices) < 3:
157 return 0.0
158
159 area = 0.0
160 n = len(self.vertices)
161 for i in range(n):
162 j = (i + 1) % n
163 area += self.vertices[i].x * self.vertices[j].y
164 area -= self.vertices[j].x * self.vertices[i].y
165 return abs(area) / 2.0
166
167 def is_convex(self) -> bool:
168 """Check if polygon is convex."""
169 if len(self.vertices) < 3:
170 return True
171
172 def cross_product(o: Point, a: Point, b: Point) -> float:
173 return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x)
174
175 n = len(self.vertices)
176 sign = None
177
178 for i in range(n):
179 cp = cross_product(
180 self.vertices[i],
181 self.vertices[(i + 1) % n],
182 self.vertices[(i + 2) % n]
183 )
184
185 if abs(cp) > 1e-10: # Not collinear
186 if sign is None:
187 sign = cp > 0
188 elif (cp > 0) != sign:
189 return False
190
191 return True
192
193 def contains_point(self, point: Point) -> bool:
194 """Check if point is inside polygon using ray casting."""
195 x, y = point.x, point.y
196 n = len(self.vertices)
197 inside = False
198
199 # First check if point is on an edge
200 for i in range(n):
201 v1 = self.vertices[i]
202 v2 = self.vertices[(i + 1) % n]
203
204 # Check if point is on the edge
205 if self._point_on_edge(point, v1, v2):
206 return True
207
208 # Ray casting algorithm
209 p1x, p1y = self.vertices[0].x, self.vertices[0].y
210 for i in range(1, n + 1):
211 p2x, p2y = self.vertices[i % n].x, self.vertices[i % n].y
212 if y > min(p1y, p2y):
213 if y <= max(p1y, p2y):
214 if x <= max(p1x, p2x):
215 if p1y != p2y:
216 xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
217 if p1x == p2x or x <= xinters:
218 inside = not inside
219 p1x, p1y = p2x, p2y
220
221 return inside
222
223 def _point_on_edge(self, point: Point, edge_start: Point, edge_end: Point) -> bool:
224 """Check if a point lies on an edge."""
225 # Use distance to line segment
226 distance = point.distance_to_line(edge_start, edge_end)
227 return distance < 1e-10
228
229 def get_sharp_angles(self, threshold_degrees: float = 30.0) -> List[int]:
230 """Find vertices with sharp angles or boundary angles.
231
232 For traditional sharp angle detection (small thresholds < 60°):
233 finds acute angles smaller than threshold.
234 For boundary angle detection (large thresholds ≥ 60°):
235 finds angles larger than threshold.
236 """
237 def calculate_interior_angle(prev_pt: Point, curr_pt: Point, next_pt: Point) -> float:
238 """Calculate the interior angle at curr_pt."""
239 # Vectors from current point to adjacent points
240 v1 = Point(prev_pt.x - curr_pt.x, prev_pt.y - curr_pt.y)
241 v2 = Point(next_pt.x - curr_pt.x, next_pt.y - curr_pt.y)
242
243 # Calculate angle using atan2 for better numerical stability
244 angle1 = math.atan2(v1.y, v1.x)
245 angle2 = math.atan2(v2.y, v2.x)
246
247 # Calculate interior angle
248 angle_diff = angle2 - angle1
249
250 # Normalize to [0, 2π]
251 while angle_diff < 0:
252 angle_diff += 2 * math.pi
253 while angle_diff > 2 * math.pi:
254 angle_diff -= 2 * math.pi
255
256 # Convert to degrees
257 interior_angle = math.degrees(angle_diff)
258
259 # For convex polygons in counter-clockwise order, interior angles should be < 180°
260 # For clockwise order, we need to take the complement
261 if interior_angle > 180:
262 interior_angle = 360 - interior_angle
263
264 return interior_angle
265
266 sharp_angles = []
267 n = len(self.vertices)
268
269 for i in range(n):
270 prev_vertex = self.vertices[(i - 1) % n]
271 curr_vertex = self.vertices[i]
272 next_vertex = self.vertices[(i + 1) % n]
273
274 interior_angle = calculate_interior_angle(prev_vertex, curr_vertex, next_vertex)
275
276 # Detect sharp angles using a range-based approach
277 # 1. Traditional sharp angles: much smaller than threshold
278 # 2. Boundary angles: slightly larger than threshold
279 tolerance = 5.0 # degrees
280
281 is_traditional_sharp = interior_angle < threshold_degrees * 0.8 # Much smaller
282 is_boundary_angle = threshold_degrees < interior_angle < threshold_degrees + tolerance # Slightly larger
283
284 if is_traditional_sharp or is_boundary_angle:
285 sharp_angles.append(i)
286
287 return sharp_angles
2D point with high-precision coordinates and utility methods
Definition point.hpp:23
double x
X coordinate.
Definition point.hpp:25
bool __eq__(self, other)
Definition geometry.py:29
double distance_to_line(const Point &line_start, const Point &line_end) const
Calculate distance from this point to a line segment.
Definition point.cpp:68
double y
Y coordinate.
Definition point.hpp:26
double distance_to(const Point &other) const
Calculate Euclidean distance to another point.
Definition point.cpp:56
__init__(self, float x, float y)
Definition geometry.py:19
Polygon class supporting both convex and concave polygons.
Definition polygon.hpp:25
std::vector< std::pair< Point, Point > > edges() const
Get polygon edges as pairs of consecutive vertices.
Definition polygon.cpp:36
Rectangle bounding_box() const
Calculate axis-aligned bounding box.
Definition polygon.cpp:84
std::vector< size_t > get_sharp_angles(double threshold_degrees=30.0) const
Find vertices with sharp angles.
Definition polygon.cpp:175
std::vector< Point > vertices
Polygon vertices in order.
Definition polygon.hpp:27
bool _point_on_edge(self, Point point, Point edge_start, Point edge_end)
Definition geometry.py:223
bool is_convex() const
Check if polygon is convex.
Definition polygon.cpp:101
__init__(self, List[Point] vertices)
Definition geometry.py:125
bool contains_point(const Point &point) const
Check if point is inside polygon (ray casting algorithm)
Definition polygon.cpp:146
double area() const
Calculate polygon area using shoelace formula.
Definition polygon.cpp:47
Axis-aligned rectangle for bounding boxes and simple EDA components.
Definition rectangle.hpp:26
double top() const
Get top edge Y coordinate.
Definition rectangle.hpp:98
double left() const
Get left edge X coordinate.
Definition rectangle.hpp:83
double y
Y coordinate of bottom-left corner.
Definition rectangle.hpp:29
Polygon to_polygon() const
Convert rectangle to polygon representation.
Point center() const
Get center point of rectangle.
Definition rectangle.cpp:68
__init__(self, float x, float y, float width, float height)
Definition geometry.py:72
double height
Height of rectangle.
Definition rectangle.hpp:31
double right() const
Get right edge X coordinate.
Definition rectangle.hpp:88
bool intersects(const Rectangle &other) const
Check if this rectangle intersects with another rectangle.
bool contains_point(const Point &point) const
Check if point is inside rectangle (inclusive of boundary)
Definition rectangle.cpp:89
double x
X coordinate of bottom-left corner.
Definition rectangle.hpp:28
double width
Width of rectangle.
Definition rectangle.hpp:30