-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsort.py
More file actions
188 lines (157 loc) · 6.57 KB
/
sort.py
File metadata and controls
188 lines (157 loc) · 6.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
import copy
import math
from dataclasses import dataclass
@dataclass
class Location:
x: float
y: float
@dataclass
class LED:
r: float
g: float
b: float
hue: float
intensity: float
sample_percent: float
location: Location
def find_xy_container(obj):
"""Find the first object that has X/x and Y/y attributes (case-insensitive)."""
attrs = {name.lower(): name for name in dir(obj)}
# Direct match
if "x" in attrs and "y" in attrs:
return obj
# Look one level down into attributes
for name in attrs.values():
if name.startswith("__") and name.endswith("__"):
continue # skip dunders
try:
nested = getattr(obj, name)
except Exception:
continue
if not hasattr(nested, "__dict__"):
continue
nested_attrs = {n.lower(): n for n in dir(nested)}
if "x" in nested_attrs and "y" in nested_attrs:
return nested
return None
def get_xy(obj):
container = find_xy_container(obj)
if container is None:
raise AttributeError(f"{type(obj).__name__} has no X/x and Y/y attributes")
attrs = {name.lower(): name for name in dir(container)}
return getattr(container, attrs["x"]), getattr(container, attrs["y"])
def set_xy(obj, x, y):
container = find_xy_container(obj)
if container is None:
raise AttributeError(f"{type(obj).__name__} has no X/x and Y/y attributes")
attrs = {name.lower(): name for name in dir(container)}
setattr(container, attrs["x"], x)
setattr(container, attrs["y"], y)
def point_line_distance(x1, y1, x2, y2, x0, y0):
A = y2 - y1
B = x1 - x2
C = x2*y1 - x1*y2
denom = math.hypot(A, B)
if denom == 0:
# endpoints are the same point; return distance to that point
return math.hypot(x0 - x1, y0 - y1)
return abs(A*x0 + B*y0 + C) / denom
def sort_by_xy(objs, threshold):
"""Sort a list of objects that have X/x and Y/y attributes.
Algorithm (iterative):
- Work on a deep copy of the list so callers' objects aren't mutated.
- While there are points remaining:
- If <= 2 points remain, append them sorted by (x, y) and finish.
- Find the top-left and top-right candidates using Y-X and X+Y heuristics.
- Build a row: include the endpoints and any points whose orthogonal distance
to the line between endpoints is < threshold.
- Sort that row left-to-right (by x then y), append to the result, and remove
those points from the remaining list.
- If a row can't be found, fall back to a simple (x, y) sort for the rest.
"""
objs = copy.deepcopy(objs)
sorted_list = []
while objs:
# Small remainder -> just append sorted by x then y
if len(objs) <= 2:
remainder_sorted = sorted(objs, key=lambda o: (get_xy(o)[0], get_xy(o)[1]))
sorted_list.extend(remainder_sorted)
break
xy_values = [get_xy(o) for o in objs]
# Prefer endpoints from the topmost row (smallest y) when possible.
try:
y_min = min(y for x, y in xy_values)
top_candidates = [i for i, (x, y) in enumerate(xy_values) if y == y_min]
if len(top_candidates) >= 2:
top_left_idx = min(top_candidates, key=lambda i: xy_values[i][0])
top_right_idx = max(top_candidates, key=lambda i: xy_values[i][0])
else:
# fallback heuristics for rotated rows or single-top-point cases
top_left_idx = max(range(len(xy_values)), key=lambda i: xy_values[i][1] - xy_values[i][0]) # Computer Max(y - x) (Maximize y, minimize x)
top_right_idx = max(range(len(xy_values)), key=lambda i: xy_values[i][0] + xy_values[i][1]) # computer Max(x + y) (Maximize x, maximize y)
except ValueError:
# no points
break
x1, y1 = xy_values[top_left_idx]
x2, y2 = xy_values[top_right_idx]
print(f"Top-left idx: {top_left_idx}, point: ({x1}, {y1})")
print(f"Top-right idx: {top_right_idx}, point: ({x2}, {y2})")
close_points = []
# Always include the endpoints
close_points.append(objs[top_left_idx])
if top_right_idx != top_left_idx:
close_points.append(objs[top_right_idx])
# Collect other points close to the line
for i, (x0, y0) in enumerate(xy_values):
if i == top_left_idx or i == top_right_idx:
continue
dist = point_line_distance(x1, y1, x2, y2, x0, y0)
if dist <= threshold:
close_points.append(objs[i])
# If we didn't collect any non-endpoint points and only have endpoints,
# fall back to a simple sort to avoid infinite loops or bad grouping.
if not close_points:
remainder_sorted = sorted(objs, key=lambda o: (get_xy(o)[0], get_xy(o)[1]))
sorted_list.extend(remainder_sorted)
break
# Sort the row left-to-right (x then y) and append to output
close_points.sort(key=lambda o: (get_xy(o)[0], get_xy(o)[1]))
sorted_list.extend(close_points)
# Remove those points from objs (compare by identity of the deepcopy)
remove_ids = {id(p) for p in close_points}
objs = [p for p in objs if id(p) not in remove_ids]
return sorted_list
def main():
# Example usage
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point(x={self.x}, y={self.y})"
# Test with more complex data structures
points = [
LED(r=255.0, g=100.0, b=50.0, hue=120.0, intensity=0.85, sample_percent=0.5, location= Location(x=1, y=2)),
LED(r=255.0, g=100.0, b=50.0, hue=120.0, intensity=0.85, sample_percent=0.5, location= Location(x=10, y=0)),
LED(r=255.0, g=100.0, b=50.0, hue=120.0, intensity=0.85, sample_percent=0.5, location= Location(x=5, y=2.00)),
]
sorted_points = sort_by_xy(points, 0.5)
for p in sorted_points:
print(p)
# Plot these points on a grid for visualization
try:
import matplotlib.pyplot as plt
xs = [p.x for p in sorted_points]
ys = [p.y for p in sorted_points]
plt.scatter(xs, ys)
for i, p in enumerate(sorted_points):
plt.text(p.x, p.y, str(i), fontsize=12, ha='right')
plt.title("Sorted Points Visualization")
plt.xlabel("X")
plt.ylabel("Y")
plt.grid(True)
plt.show()
except ImportError:
print("matplotlib not installed; skipping plot.")
if __name__ == "__main__":
main()