|
7 | 7 | #include <QSet> |
8 | 8 | #include <algorithm> |
9 | 9 | #include <cmath> |
| 10 | +#include <limits> |
10 | 11 |
|
11 | 12 | namespace PlasmaZones { |
12 | 13 |
|
@@ -84,53 +85,101 @@ ZoneDetectionResult ZoneDetector::detectZone(const QPointF& cursorPos) const |
84 | 85 |
|
85 | 86 | namespace { |
86 | 87 |
|
87 | | -// Shared raycasting algorithm: expand seed zones to include all zones that intersect |
88 | | -// the bounding rectangle. Same logic used by detectMultiZone and paint-to-snap. |
89 | | -QVector<Zone*> expandZonesByIntersection(Layout* layout, const QVector<Zone*>& seedZones) |
| 88 | +// Check if two rects share an edge (left↔right or top↔bottom) within tolerance, |
| 89 | +// with perpendicular overlap > 0. Used by both expandZonesByIntersection and areZonesAdjacent. |
| 90 | +// minOverlapFraction: minimum perpendicular overlap as fraction of the smaller dimension (0.0–1.0). |
| 91 | +bool sharesEdge(const QRectF& r1, const QRectF& r2, qreal tolerance, qreal minOverlapFraction = 0.0) |
| 92 | +{ |
| 93 | + // Left-Right adjacency (r1.right ≈ r2.left or r2.right ≈ r1.left) |
| 94 | + if (qAbs(r1.right() - r2.left()) <= tolerance || qAbs(r2.right() - r1.left()) <= tolerance) { |
| 95 | + qreal overlap = qMin(r1.bottom(), r2.bottom()) - qMax(r1.top(), r2.top()); |
| 96 | + if (overlap > 0 && overlap >= qMin(r1.height(), r2.height()) * minOverlapFraction) { |
| 97 | + return true; |
| 98 | + } |
| 99 | + } |
| 100 | + // Top-Bottom adjacency (r1.bottom ≈ r2.top or r2.bottom ≈ r1.top) |
| 101 | + if (qAbs(r1.bottom() - r2.top()) <= tolerance || qAbs(r2.bottom() - r1.top()) <= tolerance) { |
| 102 | + qreal overlap = qMin(r1.right(), r2.right()) - qMax(r1.left(), r2.left()); |
| 103 | + if (overlap > 0 && overlap >= qMin(r1.width(), r2.width()) * minOverlapFraction) { |
| 104 | + return true; |
| 105 | + } |
| 106 | + } |
| 107 | + return false; |
| 108 | +} |
| 109 | + |
| 110 | +// Check if two rects spatially overlap (intersection is thick on both axes). |
| 111 | +// Distinguishes true area-overlap from edge-touching. |
| 112 | +bool spatiallyOverlaps(const QRectF& r1, const QRectF& r2, qreal tolerance) |
| 113 | +{ |
| 114 | + QRectF intersection = r1.intersected(r2); |
| 115 | + return intersection.width() > tolerance && intersection.height() > tolerance; |
| 116 | +} |
| 117 | + |
| 118 | +// Expand seed zones by edge-adjacency flood-fill. A zone is added only if it shares |
| 119 | +// an edge with an already-selected zone AND does not spatially overlap any seed zone. |
| 120 | +// This preserves gap-filling for non-overlapping tiling layouts while preventing |
| 121 | +// cascade through stacked/overlapping zones. |
| 122 | +QVector<Zone*> expandZonesByIntersection(Layout* layout, const QVector<Zone*>& seedZones, qreal edgeTolerance) |
90 | 123 | { |
91 | 124 | if (!layout || seedZones.isEmpty()) { |
92 | 125 | return seedZones; |
93 | 126 | } |
94 | 127 |
|
95 | 128 | const auto& allZones = layout->zones(); |
96 | 129 |
|
97 | | - // Build initial bounding rect and selected set from seed zones (skip nulls) |
98 | | - QRectF boundingRect; |
| 130 | + // Build selected set and seed set from seed zones (skip nulls) |
99 | 131 | QSet<Zone*> selectedZones; |
| 132 | + QSet<Zone*> seedSet; |
100 | 133 |
|
101 | 134 | for (auto* zone : seedZones) { |
102 | 135 | if (!zone) { |
103 | 136 | continue; |
104 | 137 | } |
105 | | - if (boundingRect.isEmpty()) { |
106 | | - boundingRect = zone->geometry(); |
107 | | - } else { |
108 | | - boundingRect = boundingRect.united(zone->geometry()); |
109 | | - } |
110 | 138 | selectedZones.insert(zone); |
| 139 | + seedSet.insert(zone); |
111 | 140 | } |
112 | 141 |
|
113 | 142 | if (selectedZones.isEmpty()) { |
114 | 143 | return QVector<Zone*>(); |
115 | 144 | } |
116 | 145 |
|
117 | | - // Iteratively expand to include all zones that intersect the bounding rect |
| 146 | + // Flood-fill: add zones that share an edge with already-selected zones |
| 147 | + // but do NOT spatially overlap any seed zone |
118 | 148 | bool foundNew = true; |
119 | 149 | int iterations = 0; |
120 | 150 | const int maxIterations = 100; |
121 | 151 |
|
122 | 152 | while (foundNew && iterations < maxIterations) { |
123 | 153 | foundNew = false; |
124 | 154 | iterations++; |
125 | | - QRectF currentRect = boundingRect; |
126 | 155 |
|
127 | | - for (auto* zone : allZones) { |
128 | | - if (!zone || selectedZones.contains(zone)) { |
| 156 | + for (auto* candidate : allZones) { |
| 157 | + if (!candidate || selectedZones.contains(candidate)) { |
129 | 158 | continue; |
130 | 159 | } |
131 | | - if (zone->geometry().intersects(currentRect)) { |
132 | | - selectedZones.insert(zone); |
133 | | - boundingRect = boundingRect.united(zone->geometry()); |
| 160 | + |
| 161 | + // Skip candidates that spatially overlap any seed zone |
| 162 | + bool overlapsASeed = false; |
| 163 | + for (auto* seed : seedSet) { |
| 164 | + if (spatiallyOverlaps(candidate->geometry(), seed->geometry(), edgeTolerance)) { |
| 165 | + overlapsASeed = true; |
| 166 | + break; |
| 167 | + } |
| 168 | + } |
| 169 | + if (overlapsASeed) { |
| 170 | + continue; |
| 171 | + } |
| 172 | + |
| 173 | + // Check if candidate shares an edge with any already-selected zone |
| 174 | + bool shouldAdd = false; |
| 175 | + for (auto* selected : selectedZones) { |
| 176 | + if (sharesEdge(candidate->geometry(), selected->geometry(), edgeTolerance)) { |
| 177 | + shouldAdd = true; |
| 178 | + break; |
| 179 | + } |
| 180 | + } |
| 181 | + if (shouldAdd) { |
| 182 | + selectedZones.insert(candidate); |
134 | 183 | foundNew = true; |
135 | 184 | } |
136 | 185 | } |
@@ -160,50 +209,75 @@ ZoneDetectionResult ZoneDetector::detectMultiZone(const QPointF& cursorPos) cons |
160 | 209 | return detectZone(cursorPos); |
161 | 210 | } |
162 | 211 |
|
163 | | - // Find all zones near the cursor (within threshold or containing cursor) |
164 | | - QVector<Zone*> nearbyZones; |
165 | 212 | const auto& allZones = m_layout->zones(); |
| 213 | + const qreal adjacentThreshold = m_settings->adjacentThreshold(); |
| 214 | + |
| 215 | + // Separate overlapping zones (cursor inside) from edge-adjacent zones |
| 216 | + // (cursor outside but within threshold). Only edge-adjacent zones trigger multi-zone. |
| 217 | + QVector<Zone*> overlappingZones; |
| 218 | + QVector<Zone*> edgeAdjacentZones; |
166 | 219 |
|
167 | 220 | for (auto* zone : allZones) { |
168 | 221 | if (!zone) { |
169 | 222 | continue; |
170 | 223 | } |
171 | | - qreal distance = zone->distanceToPoint(cursorPos); |
172 | | - if (distance <= m_settings->adjacentThreshold() || zone->containsPoint(cursorPos)) { |
173 | | - nearbyZones.append(zone); |
| 224 | + if (zone->containsPoint(cursorPos)) { |
| 225 | + overlappingZones.append(zone); |
| 226 | + } else { |
| 227 | + qreal distance = zone->distanceToPoint(cursorPos); |
| 228 | + if (distance <= adjacentThreshold) { |
| 229 | + edgeAdjacentZones.append(zone); |
| 230 | + } |
174 | 231 | } |
175 | 232 | } |
176 | 233 |
|
177 | | - // If we found 2+ nearby zones, use raycast algorithm |
178 | | - if (nearbyZones.size() >= 2) { |
179 | | - Zone* primaryZone = nullptr; |
180 | | - qreal minDistance = std::numeric_limits<qreal>::max(); |
181 | | - for (auto* zone : nearbyZones) { |
182 | | - qreal distance = zone->distanceToPoint(cursorPos); |
183 | | - if (distance < minDistance) { |
184 | | - minDistance = distance; |
185 | | - primaryZone = zone; |
186 | | - } |
| 234 | + // Get primary zone via smallest-area heuristic (handles overlap correctly) |
| 235 | + Zone* primaryZone = zoneAtPoint(cursorPos); |
| 236 | + |
| 237 | + // Store overlap info for callers |
| 238 | + result.overlappingZones = overlappingZones; |
| 239 | + |
| 240 | + // Multi-zone ONLY if there are edge-adjacent zones (cursor near a boundary between zones) |
| 241 | + if (!edgeAdjacentZones.isEmpty()) { |
| 242 | + // Combine primary (if any) + edge-adjacent zones as seeds for expansion |
| 243 | + QVector<Zone*> seedZones = edgeAdjacentZones; |
| 244 | + if (primaryZone && !seedZones.contains(primaryZone)) { |
| 245 | + seedZones.prepend(primaryZone); |
187 | 246 | } |
188 | 247 |
|
189 | | - QVector<Zone*> zonesInRect = expandZonesByIntersection(m_layout, nearbyZones); |
| 248 | + QVector<Zone*> expanded = expandZonesByIntersection(m_layout, seedZones, adjacentThreshold); |
| 249 | + |
| 250 | + if (expanded.size() > 1) { |
| 251 | + if (!primaryZone) { |
| 252 | + // No containing zone — pick closest edge-adjacent as primary |
| 253 | + qreal minDistance = std::numeric_limits<qreal>::max(); |
| 254 | + for (auto* zone : edgeAdjacentZones) { |
| 255 | + qreal distance = zone->distanceToPoint(cursorPos); |
| 256 | + if (distance < minDistance) { |
| 257 | + minDistance = distance; |
| 258 | + primaryZone = zone; |
| 259 | + } |
| 260 | + } |
| 261 | + } |
190 | 262 |
|
191 | | - if (zonesInRect.size() > 1 && primaryZone) { |
192 | 263 | result.primaryZone = primaryZone; |
193 | | - result.adjacentZones = zonesInRect; |
| 264 | + result.adjacentZones = expanded; |
194 | 265 | result.isMultiZone = true; |
195 | | - result.snapGeometry = combineZoneGeometries(zonesInRect); |
| 266 | + result.snapGeometry = combineZoneGeometries(expanded); |
196 | 267 | result.distance = 0; |
197 | 268 | return result; |
198 | 269 | } |
199 | 270 | } |
200 | 271 |
|
201 | | - return detectZone(cursorPos); |
| 272 | + // Fall through to single-zone detection, preserving overlap info |
| 273 | + ZoneDetectionResult singleResult = detectZone(cursorPos); |
| 274 | + singleResult.overlappingZones = overlappingZones; |
| 275 | + return singleResult; |
202 | 276 | } |
203 | 277 |
|
204 | 278 | QVector<Zone*> ZoneDetector::expandPaintedZonesToRect(const QVector<Zone*>& seedZones) const |
205 | 279 | { |
206 | | - return expandZonesByIntersection(m_layout, seedZones); |
| 280 | + return expandZonesByIntersection(m_layout, seedZones, m_settings->adjacentThreshold()); |
207 | 281 | } |
208 | 282 |
|
209 | 283 | Zone* ZoneDetector::zoneAtPoint(const QPointF& point) const |
@@ -266,37 +340,7 @@ bool ZoneDetector::areZonesAdjacent(Zone* zone1, Zone* zone2) const |
266 | 340 | return false; |
267 | 341 | } |
268 | 342 |
|
269 | | - const QRectF& r1 = zone1->geometry(); |
270 | | - const QRectF& r2 = zone2->geometry(); |
271 | | - |
272 | | - // Check if zones share an edge (within threshold) |
273 | | - // Use a stricter threshold for adjacency (adjacentThreshold is for cursor proximity, not zone adjacency) |
274 | | - // Zones are adjacent if they share an edge within 5 pixels (much stricter than cursor proximity) |
275 | | - qreal adjacencyTolerance = 5.0; |
276 | | - |
277 | | - // Left-Right adjacency (vertical edge between zones) |
278 | | - if (qAbs(r1.right() - r2.left()) <= adjacencyTolerance || qAbs(r2.right() - r1.left()) <= adjacencyTolerance) { |
279 | | - // Check vertical overlap - zones must overlap significantly, not just touch |
280 | | - qreal overlap = qMin(r1.bottom(), r2.bottom()) - qMax(r1.top(), r2.top()); |
281 | | - qreal minHeight = qMin(r1.height(), r2.height()); |
282 | | - // Require at least 10% overlap to consider zones adjacent |
283 | | - if (overlap > 0 && overlap >= minHeight * 0.1) { |
284 | | - return true; |
285 | | - } |
286 | | - } |
287 | | - |
288 | | - // Top-Bottom adjacency (horizontal edge between zones) |
289 | | - if (qAbs(r1.bottom() - r2.top()) <= adjacencyTolerance || qAbs(r2.bottom() - r1.top()) <= adjacencyTolerance) { |
290 | | - // Check horizontal overlap - zones must overlap significantly, not just touch |
291 | | - qreal overlap = qMin(r1.right(), r2.right()) - qMax(r1.left(), r2.left()); |
292 | | - qreal minWidth = qMin(r1.width(), r2.width()); |
293 | | - // Require at least 10% overlap to consider zones adjacent |
294 | | - if (overlap > 0 && overlap >= minWidth * 0.1) { |
295 | | - return true; |
296 | | - } |
297 | | - } |
298 | | - |
299 | | - return false; |
| 343 | + return sharesEdge(zone1->geometry(), zone2->geometry(), m_settings->adjacentThreshold(), 0.1); |
300 | 344 | } |
301 | 345 |
|
302 | 346 | qreal ZoneDetector::distanceToZoneEdge(const QPointF& point, Zone* zone) const |
|
0 commit comments