|
| 1 | +#include "application/LayoutCanvasSnapping.h" |
| 2 | + |
| 3 | +#include <algorithm> |
| 4 | +#include <cmath> |
| 5 | +#include <limits> |
| 6 | +#include <vector> |
| 7 | + |
| 8 | +namespace safecrowd::application { |
| 9 | +namespace { |
| 10 | + |
| 11 | +bool matchesFloor(const std::string& elementFloorId, const std::string& floorId) { |
| 12 | + return floorId.empty() || elementFloorId.empty() || elementFloorId == floorId; |
| 13 | +} |
| 14 | + |
| 15 | +safecrowd::domain::Point2D operator-(const safecrowd::domain::Point2D& lhs, const safecrowd::domain::Point2D& rhs) { |
| 16 | + return {.x = lhs.x - rhs.x, .y = lhs.y - rhs.y}; |
| 17 | +} |
| 18 | + |
| 19 | +safecrowd::domain::Point2D operator+(const safecrowd::domain::Point2D& lhs, const safecrowd::domain::Point2D& rhs) { |
| 20 | + return {.x = lhs.x + rhs.x, .y = lhs.y + rhs.y}; |
| 21 | +} |
| 22 | + |
| 23 | +safecrowd::domain::Point2D operator*(const safecrowd::domain::Point2D& point, double scalar) { |
| 24 | + return {.x = point.x * scalar, .y = point.y * scalar}; |
| 25 | +} |
| 26 | + |
| 27 | +double dot(const safecrowd::domain::Point2D& lhs, const safecrowd::domain::Point2D& rhs) { |
| 28 | + return (lhs.x * rhs.x) + (lhs.y * rhs.y); |
| 29 | +} |
| 30 | + |
| 31 | +double screenDistance( |
| 32 | + const LayoutCanvasTransform& transform, |
| 33 | + const safecrowd::domain::Point2D& lhs, |
| 34 | + const safecrowd::domain::Point2D& rhs) { |
| 35 | + const auto a = transform.map(lhs); |
| 36 | + const auto b = transform.map(rhs); |
| 37 | + return std::hypot(a.x() - b.x(), a.y() - b.y()); |
| 38 | +} |
| 39 | + |
| 40 | +safecrowd::domain::Point2D closestPointOnSegment( |
| 41 | + const safecrowd::domain::Point2D& point, |
| 42 | + const safecrowd::domain::Point2D& start, |
| 43 | + const safecrowd::domain::Point2D& end) { |
| 44 | + const auto segment = end - start; |
| 45 | + const auto lengthSquared = dot(segment, segment); |
| 46 | + if (lengthSquared <= 1e-9) { |
| 47 | + return start; |
| 48 | + } |
| 49 | + |
| 50 | + const auto t = std::clamp(dot(point - start, segment) / lengthSquared, 0.0, 1.0); |
| 51 | + return start + (segment * t); |
| 52 | +} |
| 53 | + |
| 54 | +void appendPolygonSnapGeometry( |
| 55 | + const safecrowd::domain::Polygon2D& polygon, |
| 56 | + std::vector<safecrowd::domain::Point2D>& vertices, |
| 57 | + std::vector<safecrowd::domain::LineSegment2D>& edges) { |
| 58 | + const auto appendRing = [&](const std::vector<safecrowd::domain::Point2D>& ring) { |
| 59 | + if (ring.empty()) { |
| 60 | + return; |
| 61 | + } |
| 62 | + vertices.insert(vertices.end(), ring.begin(), ring.end()); |
| 63 | + if (ring.size() < 2) { |
| 64 | + return; |
| 65 | + } |
| 66 | + for (std::size_t index = 0; index < ring.size(); ++index) { |
| 67 | + edges.push_back({ |
| 68 | + .start = ring[index], |
| 69 | + .end = ring[(index + 1) % ring.size()], |
| 70 | + }); |
| 71 | + } |
| 72 | + }; |
| 73 | + |
| 74 | + appendRing(polygon.outline); |
| 75 | + for (const auto& hole : polygon.holes) { |
| 76 | + appendRing(hole); |
| 77 | + } |
| 78 | +} |
| 79 | + |
| 80 | +void appendPolylineSnapGeometry( |
| 81 | + const safecrowd::domain::Polyline2D& polyline, |
| 82 | + std::vector<safecrowd::domain::Point2D>& vertices, |
| 83 | + std::vector<safecrowd::domain::LineSegment2D>& edges) { |
| 84 | + vertices.insert(vertices.end(), polyline.vertices.begin(), polyline.vertices.end()); |
| 85 | + if (polyline.vertices.size() < 2) { |
| 86 | + return; |
| 87 | + } |
| 88 | + |
| 89 | + for (std::size_t index = 1; index < polyline.vertices.size(); ++index) { |
| 90 | + edges.push_back({ |
| 91 | + .start = polyline.vertices[index - 1], |
| 92 | + .end = polyline.vertices[index], |
| 93 | + }); |
| 94 | + } |
| 95 | + if (polyline.closed && polyline.vertices.size() > 2) { |
| 96 | + edges.push_back({ |
| 97 | + .start = polyline.vertices.back(), |
| 98 | + .end = polyline.vertices.front(), |
| 99 | + }); |
| 100 | + } |
| 101 | +} |
| 102 | + |
| 103 | +} // namespace |
| 104 | + |
| 105 | +LayoutSnapResult snapLayoutPoint( |
| 106 | + const safecrowd::domain::FacilityLayout2D& layout, |
| 107 | + const std::string& floorId, |
| 108 | + const safecrowd::domain::Point2D& point, |
| 109 | + const LayoutCanvasTransform& transform, |
| 110 | + const LayoutSnapOptions& options) { |
| 111 | + std::vector<safecrowd::domain::Point2D> vertices; |
| 112 | + std::vector<safecrowd::domain::LineSegment2D> edges; |
| 113 | + |
| 114 | + for (const auto& zone : layout.zones) { |
| 115 | + if (matchesFloor(zone.floorId, floorId)) { |
| 116 | + appendPolygonSnapGeometry(zone.area, vertices, edges); |
| 117 | + } |
| 118 | + } |
| 119 | + for (const auto& barrier : layout.barriers) { |
| 120 | + if (matchesFloor(barrier.floorId, floorId)) { |
| 121 | + appendPolylineSnapGeometry(barrier.geometry, vertices, edges); |
| 122 | + } |
| 123 | + } |
| 124 | + for (const auto& connection : layout.connections) { |
| 125 | + if (!matchesFloor(connection.floorId, floorId)) { |
| 126 | + continue; |
| 127 | + } |
| 128 | + vertices.push_back(connection.centerSpan.start); |
| 129 | + vertices.push_back(connection.centerSpan.end); |
| 130 | + edges.push_back(connection.centerSpan); |
| 131 | + } |
| 132 | + |
| 133 | + LayoutSnapResult result{.point = point}; |
| 134 | + double bestDistance = options.tolerancePixels; |
| 135 | + |
| 136 | + if (options.snapVertices) { |
| 137 | + for (const auto& vertex : vertices) { |
| 138 | + const auto distance = screenDistance(transform, point, vertex); |
| 139 | + if (distance <= bestDistance) { |
| 140 | + bestDistance = distance; |
| 141 | + result = {.point = vertex, .snapped = true}; |
| 142 | + } |
| 143 | + } |
| 144 | + } |
| 145 | + |
| 146 | + if (options.snapEdges) { |
| 147 | + for (const auto& edge : edges) { |
| 148 | + const auto candidate = closestPointOnSegment(point, edge.start, edge.end); |
| 149 | + const auto distance = screenDistance(transform, point, candidate); |
| 150 | + if (distance <= bestDistance) { |
| 151 | + bestDistance = distance; |
| 152 | + result = {.point = candidate, .snapped = true}; |
| 153 | + } |
| 154 | + } |
| 155 | + } |
| 156 | + |
| 157 | + return result; |
| 158 | +} |
| 159 | + |
| 160 | +} // namespace safecrowd::application |
0 commit comments