Skip to content

Commit 26d83cd

Browse files
committed
stair
1 parent 843db72 commit 26d83cd

5 files changed

Lines changed: 218 additions & 7 deletions

File tree

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ if (SAFECROWD_BUILD_APP)
175175
add_executable(safecrowd_app
176176
src/application/IssueCardWidget.h
177177
src/application/LayoutCanvasRendering.h
178+
src/application/LayoutCanvasSnapping.h
178179
src/application/LayoutNavigationPanelWidget.h
179180
src/application/LayoutPreviewWidget.h
180181
src/application/LayoutReviewWidget.h
@@ -196,6 +197,7 @@ if (SAFECROWD_BUILD_APP)
196197
src/application/WorkspaceShell.h
197198
src/application/IssueCardWidget.cpp
198199
src/application/LayoutCanvasRendering.cpp
200+
src/application/LayoutCanvasSnapping.cpp
199201
src/application/LayoutNavigationPanelWidget.cpp
200202
src/application/LayoutPreviewWidget.cpp
201203
src/application/LayoutReviewWidget.cpp
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#pragma once
2+
3+
#include <string>
4+
5+
#include "application/LayoutCanvasRendering.h"
6+
#include "domain/FacilityLayout2D.h"
7+
8+
namespace safecrowd::application {
9+
10+
struct LayoutSnapOptions {
11+
double tolerancePixels{12.0};
12+
bool snapVertices{true};
13+
bool snapEdges{true};
14+
};
15+
16+
struct LayoutSnapResult {
17+
safecrowd::domain::Point2D point{};
18+
bool snapped{false};
19+
};
20+
21+
LayoutSnapResult snapLayoutPoint(
22+
const safecrowd::domain::FacilityLayout2D& layout,
23+
const std::string& floorId,
24+
const safecrowd::domain::Point2D& point,
25+
const LayoutCanvasTransform& transform,
26+
const LayoutSnapOptions& options = {});
27+
28+
} // namespace safecrowd::application

src/application/LayoutPreviewWidget.cpp

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include <optional>
77

88
#include "application/LayoutCanvasRendering.h"
9+
#include "application/LayoutCanvasSnapping.h"
910

1011
#include <QCoreApplication>
1112
#include <QCheckBox>
@@ -1399,7 +1400,7 @@ void LayoutPreviewWidget::mouseMoveEvent(QMouseEvent* event) {
13991400
if (bounds.has_value()) {
14001401
const LayoutTransform transform(*bounds, previewViewport(rect()), camera_.zoom(), camera_.panOffset());
14011402
const auto world = transform.unmap(event->position());
1402-
draftCurrentWorld_ = QPointF(world.x, world.y);
1403+
draftCurrentWorld_ = snapWorldPoint(QPointF(world.x, world.y), transform);
14031404
update();
14041405
event->accept();
14051406
return;
@@ -1438,7 +1439,7 @@ void LayoutPreviewWidget::mousePressEvent(QMouseEvent* event) {
14381439
const LayoutTransform transform(*bounds, previewViewport(rect()), camera_.zoom(), camera_.panOffset());
14391440
const auto world = transform.unmap(event->position());
14401441
drafting_ = true;
1441-
draftStartWorld_ = QPointF(world.x, world.y);
1442+
draftStartWorld_ = snapWorldPoint(QPointF(world.x, world.y), transform);
14421443
draftCurrentWorld_ = draftStartWorld_;
14431444
event->accept();
14441445
return;
@@ -1463,7 +1464,7 @@ void LayoutPreviewWidget::mouseReleaseEvent(QMouseEvent* event) {
14631464
if (bounds.has_value()) {
14641465
const LayoutTransform transform(*bounds, previewViewport(rect()), camera_.zoom(), camera_.panOffset());
14651466
const auto world = transform.unmap(event->position());
1466-
draftCurrentWorld_ = QPointF(world.x, world.y);
1467+
draftCurrentWorld_ = snapWorldPoint(QPointF(world.x, world.y), transform);
14671468
}
14681469

14691470
switch (toolMode_) {
@@ -1671,9 +1672,15 @@ void LayoutPreviewWidget::applyToolAt(const QPointF& position) {
16711672

16721673
const LayoutTransform transform(*bounds, previewViewport(rect()), camera_.zoom(), camera_.panOffset());
16731674
const auto floorId = currentFloorId();
1674-
const auto zoneId = hitTestZone(*importResult_.layout, position, transform, floorId);
1675-
const auto connectionId = hitTestConnection(*importResult_.layout, position, transform, floorId);
1676-
const auto barrierId = hitTestBarrier(*importResult_.layout, position, transform, floorId);
1675+
QPointF testPosition = position;
1676+
if (toolMode_ == ToolMode::DrawDoor) {
1677+
const auto world = transform.unmap(position);
1678+
const auto snappedWorld = snapWorldPoint(QPointF(world.x, world.y), transform);
1679+
testPosition = transform.map({.x = snappedWorld.x(), .y = snappedWorld.y()});
1680+
}
1681+
const auto zoneId = hitTestZone(*importResult_.layout, testPosition, transform, floorId);
1682+
const auto connectionId = hitTestConnection(*importResult_.layout, testPosition, transform, floorId);
1683+
const auto barrierId = hitTestBarrier(*importResult_.layout, testPosition, transform, floorId);
16771684

16781685
switch (toolMode_) {
16791686
case ToolMode::Select:
@@ -1703,7 +1710,7 @@ void LayoutPreviewWidget::applyToolAt(const QPointF& position) {
17031710
if (!barrierId.has_value()) {
17041711
return;
17051712
}
1706-
const auto world = transform.unmap(position);
1713+
const auto world = transform.unmap(testPosition);
17071714
createDoorAt(*barrierId, QPointF(world.x, world.y));
17081715
return;
17091716
}
@@ -2213,6 +2220,19 @@ void LayoutPreviewWidget::emitCurrentSelection() {
22132220
}
22142221
}
22152222

2223+
QPointF LayoutPreviewWidget::snapWorldPoint(const QPointF& worldPoint, const LayoutCanvasTransform& transform) const {
2224+
if (!importResult_.layout.has_value()) {
2225+
return worldPoint;
2226+
}
2227+
2228+
const auto snapped = snapLayoutPoint(
2229+
*importResult_.layout,
2230+
currentFloorId().toStdString(),
2231+
{.x = worldPoint.x(), .y = worldPoint.y()},
2232+
transform);
2233+
return QPointF(snapped.point.x, snapped.point.y);
2234+
}
2235+
22162236
void LayoutPreviewWidget::notifyLayoutEdited() {
22172237
if (layoutEditedHandler_ && importResult_.layout.has_value()) {
22182238
layoutEditedHandler_(*importResult_.layout);

src/application/LayoutPreviewWidget.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ class LayoutPreviewWidget : public QWidget {
8484
void deleteConnection(const QString& connectionId);
8585
void deleteBarrier(const QString& barrierId);
8686
void emitCurrentSelection();
87+
QPointF snapWorldPoint(const QPointF& worldPoint, const LayoutCanvasTransform& transform) const;
8788
void notifyLayoutEdited();
8889
void repositionToolbars();
8990
void refreshFloorSelector();

0 commit comments

Comments
 (0)