Skip to content

Commit 7009cc7

Browse files
authored
[Domain] Prevent narrow-door wall sticking
Closes #174
1 parent 1904c73 commit 7009cc7

5 files changed

Lines changed: 825 additions & 65 deletions

File tree

src/application/LayoutPreviewWidget.cpp

Lines changed: 103 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,12 @@ QPointF entryOutsideSample(const QRectF& rectangle, safecrowd::domain::StairEntr
449449
std::optional<std::vector<std::pair<safecrowd::domain::Point2D, safecrowd::domain::Point2D>>> barrierSegmentsAfterGap(
450450
const safecrowd::domain::Barrier2D& barrier,
451451
const safecrowd::domain::LineSegment2D& gap);
452+
bool spanOverlapsPolygonBoundary(
453+
const safecrowd::domain::Polygon2D& polygon,
454+
const safecrowd::domain::LineSegment2D& span);
455+
std::optional<QPointF> outsideSampleForBoundarySpan(
456+
const safecrowd::domain::Polygon2D& polygon,
457+
const safecrowd::domain::LineSegment2D& span);
452458

453459
bool isVerticalLink(const safecrowd::domain::Connection2D& connection) {
454460
return connection.kind == safecrowd::domain::ConnectionKind::Stair
@@ -470,6 +476,23 @@ const safecrowd::domain::Zone2D* findZoneById(
470476
return it == layout.zones.end() ? nullptr : &(*it);
471477
}
472478

479+
bool connectionVisibleOnFloor(
480+
const safecrowd::domain::FacilityLayout2D& layout,
481+
const safecrowd::domain::Connection2D& connection,
482+
const QString& floorId) {
483+
if (matchesFloor(connection.floorId, floorId)) {
484+
return true;
485+
}
486+
if (!isVerticalLink(connection)) {
487+
return false;
488+
}
489+
490+
const auto* fromZone = findZoneById(layout, connection.fromZoneId);
491+
const auto* toZone = findZoneById(layout, connection.toZoneId);
492+
return (fromZone != nullptr && matchesFloor(fromZone->floorId, floorId))
493+
|| (toZone != nullptr && matchesFloor(toZone->floorId, floorId));
494+
}
495+
473496
std::optional<std::size_t> findZoneIndexById(
474497
const safecrowd::domain::FacilityLayout2D& layout,
475498
const std::string& zoneId) {
@@ -517,8 +540,7 @@ std::optional<safecrowd::domain::LineSegment2D> stairEntrySpanForFloor(
517540
const safecrowd::domain::FacilityLayout2D& layout,
518541
const safecrowd::domain::Connection2D& connection,
519542
const std::string& floorId) {
520-
const auto direction = stairEntryDirectionForFloor(layout, connection, floorId);
521-
if (!direction.has_value()) {
543+
if (!isVerticalLink(connection)) {
522544
return std::nullopt;
523545
}
524546

@@ -529,6 +551,15 @@ std::optional<safecrowd::domain::LineSegment2D> stairEntrySpanForFloor(
529551
return std::nullopt;
530552
}
531553

554+
if (spanOverlapsPolygonBoundary(stairZone->area, connection.centerSpan)) {
555+
return connection.centerSpan;
556+
}
557+
558+
const auto direction = stairEntryDirectionForFloor(layout, connection, floorId);
559+
if (!direction.has_value()) {
560+
return std::nullopt;
561+
}
562+
532563
const auto bounds = polygonBounds(stairZone->area);
533564
if (!bounds.valid()) {
534565
return std::nullopt;
@@ -542,8 +573,7 @@ std::optional<QPointF> stairEntryOutsideSampleForFloor(
542573
const safecrowd::domain::FacilityLayout2D& layout,
543574
const safecrowd::domain::Connection2D& connection,
544575
const std::string& floorId) {
545-
const auto direction = stairEntryDirectionForFloor(layout, connection, floorId);
546-
if (!direction.has_value()) {
576+
if (!isVerticalLink(connection)) {
547577
return std::nullopt;
548578
}
549579

@@ -554,6 +584,15 @@ std::optional<QPointF> stairEntryOutsideSampleForFloor(
554584
return std::nullopt;
555585
}
556586

587+
if (spanOverlapsPolygonBoundary(stairZone->area, connection.centerSpan)) {
588+
return outsideSampleForBoundarySpan(stairZone->area, connection.centerSpan);
589+
}
590+
591+
const auto direction = stairEntryDirectionForFloor(layout, connection, floorId);
592+
if (!direction.has_value()) {
593+
return std::nullopt;
594+
}
595+
557596
const auto bounds = polygonBounds(stairZone->area);
558597
if (!bounds.valid()) {
559598
return std::nullopt;
@@ -714,6 +753,62 @@ bool pointInPolygon(const safecrowd::domain::Polygon2D& polygon, const QPointF&
714753
return true;
715754
}
716755

756+
bool spanOverlapsPolygonBoundary(
757+
const safecrowd::domain::Polygon2D& polygon,
758+
const safecrowd::domain::LineSegment2D& span) {
759+
const auto spanLength = std::hypot(span.end.x - span.start.x, span.end.y - span.start.y);
760+
if (spanLength <= kGeometryEpsilon) {
761+
return false;
762+
}
763+
764+
const auto checkRing = [&](const auto& ring) {
765+
if (ring.size() < 2) {
766+
return false;
767+
}
768+
for (std::size_t index = 0; index < ring.size(); ++index) {
769+
if (segmentsShareSpan(span, ring[index], ring[(index + 1) % ring.size()])) {
770+
return true;
771+
}
772+
}
773+
return false;
774+
};
775+
776+
if (checkRing(polygon.outline)) {
777+
return true;
778+
}
779+
return std::any_of(polygon.holes.begin(), polygon.holes.end(), checkRing);
780+
}
781+
782+
std::optional<QPointF> outsideSampleForBoundarySpan(
783+
const safecrowd::domain::Polygon2D& polygon,
784+
const safecrowd::domain::LineSegment2D& span) {
785+
const auto dx = span.end.x - span.start.x;
786+
const auto dy = span.end.y - span.start.y;
787+
const auto length = std::hypot(dx, dy);
788+
if (length <= kGeometryEpsilon) {
789+
return std::nullopt;
790+
}
791+
792+
const QPointF center(
793+
(span.start.x + span.end.x) * 0.5,
794+
(span.start.y + span.end.y) * 0.5);
795+
constexpr double sampleOffset = 0.45;
796+
const QPointF normalA(-dy / length * sampleOffset, dx / length * sampleOffset);
797+
const QPointF normalB(dy / length * sampleOffset, -dx / length * sampleOffset);
798+
const QPointF sampleA = center + normalA;
799+
const QPointF sampleB = center + normalB;
800+
const bool sampleAInside = pointInPolygon(polygon, sampleA);
801+
const bool sampleBInside = pointInPolygon(polygon, sampleB);
802+
803+
if (!sampleAInside && sampleBInside) {
804+
return sampleA;
805+
}
806+
if (!sampleBInside && sampleAInside) {
807+
return sampleB;
808+
}
809+
return std::nullopt;
810+
}
811+
717812
double distanceToLineSegmentWorld(const QPointF& point, const safecrowd::domain::Point2D& start, const safecrowd::domain::Point2D& end) {
718813
return distanceToSegment(point, QPointF(start.x, start.y), QPointF(end.x, end.y));
719814
}
@@ -1970,7 +2065,7 @@ std::optional<QString> hitTestConnection(
19702065
std::optional<QString> bestId;
19712066

19722067
for (const auto& connection : layout.connections) {
1973-
if (!matchesFloor(connection.floorId, floorId)) {
2068+
if (!connectionVisibleOnFloor(layout, connection, floorId)) {
19742069
continue;
19752070
}
19762071
const auto start = transform.map(connection.centerSpan.start);
@@ -2057,7 +2152,7 @@ bool selectedConnectionContainsContextPoint(
20572152

20582153
for (const auto& connection : layout.connections) {
20592154
const auto connectionId = QString::fromStdString(connection.id);
2060-
if (!selectedConnectionIds.contains(connectionId) || !matchesFloor(connection.floorId, floorId)) {
2155+
if (!selectedConnectionIds.contains(connectionId) || !connectionVisibleOnFloor(layout, connection, floorId)) {
20612156
continue;
20622157
}
20632158

@@ -2744,7 +2839,7 @@ void LayoutPreviewWidget::paintEvent(QPaintEvent* event) {
27442839
}
27452840
}
27462841
for (const auto& connection : importResult_.layout->connections) {
2747-
if (!matchesFloor(connection.floorId, currentFloorId())) {
2842+
if (!connectionVisibleOnFloor(*importResult_.layout, connection, currentFloorId())) {
27482843
continue;
27492844
}
27502845
const auto id = QString::fromStdString(connection.id);
@@ -4336,7 +4431,7 @@ void LayoutPreviewWidget::selectElementsInRect(const QRectF& screenRect, const L
43364431
}
43374432
}
43384433
for (const auto& connection : layout.connections) {
4339-
if (!matchesFloor(connection.floorId, floorId)) {
4434+
if (!connectionVisibleOnFloor(layout, connection, floorId)) {
43404435
continue;
43414436
}
43424437
if (strokedPathIntersectsRect(linePath(connection.centerSpan, transform), screenRect, kSelectionStrokeWidthPixels)) {

src/domain/ScenarioSimulationInternal.cpp

Lines changed: 94 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,73 @@ std::vector<LineSegment2D> stairEntryBarrierSegments(const Zone2D& zone, StairEn
376376
return segments;
377377
}
378378

379+
std::vector<LineSegment2D> barrierSegmentAfterConnectionGap(
380+
const LineSegment2D& segment,
381+
const LineSegment2D& gap) {
382+
const auto segmentDelta = segment.end - segment.start;
383+
const auto segmentLength = lengthOf(segmentDelta);
384+
if (segmentLength <= kGeometryEpsilon) {
385+
return {};
386+
}
387+
388+
if (std::fabs(cross(segment.start, segment.end, gap.start)) > kGeometryEpsilon
389+
|| std::fabs(cross(segment.start, segment.end, gap.end)) > kGeometryEpsilon) {
390+
return {segment};
391+
}
392+
393+
const auto direction = segmentDelta * (1.0 / segmentLength);
394+
auto gapStart = dot(gap.start - segment.start, direction);
395+
auto gapEnd = dot(gap.end - segment.start, direction);
396+
if (gapStart > gapEnd) {
397+
std::swap(gapStart, gapEnd);
398+
}
399+
400+
const auto overlapStart = std::max(0.0, gapStart);
401+
const auto overlapEnd = std::min(segmentLength, gapEnd);
402+
if (overlapEnd <= overlapStart + kGeometryEpsilon) {
403+
return {segment};
404+
}
405+
406+
std::vector<LineSegment2D> remaining;
407+
if (overlapStart > kGeometryEpsilon) {
408+
remaining.push_back({
409+
.start = segment.start,
410+
.end = segment.start + (direction * overlapStart),
411+
});
412+
}
413+
if (overlapEnd < segmentLength - kGeometryEpsilon) {
414+
remaining.push_back({
415+
.start = segment.start + (direction * overlapEnd),
416+
.end = segment.end,
417+
});
418+
}
419+
return remaining;
420+
}
421+
422+
std::vector<LineSegment2D> clipStairEntryBarrierSegment(
423+
const LineSegment2D& segment,
424+
const FacilityLayout2D& source,
425+
const Zone2D& stairZone) {
426+
std::vector<LineSegment2D> remaining{segment};
427+
for (const auto& connection : source.connections) {
428+
if (!isVerticalConnection(connection)
429+
|| (connection.fromZoneId != stairZone.id && connection.toZoneId != stairZone.id)) {
430+
continue;
431+
}
432+
433+
std::vector<LineSegment2D> next;
434+
for (const auto& candidate : remaining) {
435+
auto clipped = barrierSegmentAfterConnectionGap(candidate, connection.centerSpan);
436+
next.insert(next.end(), clipped.begin(), clipped.end());
437+
}
438+
remaining = std::move(next);
439+
if (remaining.empty()) {
440+
break;
441+
}
442+
}
443+
return remaining;
444+
}
445+
379446
void appendStairEntryBarriers(FacilityLayout2D& filtered, const FacilityLayout2D& source, const std::string& floorId) {
380447
int suffix = 1;
381448
for (const auto& connection : source.connections) {
@@ -394,12 +461,14 @@ void appendStairEntryBarriers(FacilityLayout2D& filtered, const FacilityLayout2D
394461
}
395462

396463
for (const auto& segment : stairEntryBarrierSegments(*stairZone, direction)) {
397-
filtered.barriers.push_back({
398-
.id = connection.id + "-entry-wall-" + std::to_string(suffix++),
399-
.floorId = stairZone->floorId,
400-
.geometry = {.vertices = {segment.start, segment.end}},
401-
.blocksMovement = true,
402-
});
464+
for (const auto& clippedSegment : clipStairEntryBarrierSegment(segment, source, *stairZone)) {
465+
filtered.barriers.push_back({
466+
.id = connection.id + "-entry-wall-" + std::to_string(suffix++),
467+
.floorId = stairZone->floorId,
468+
.geometry = {.vertices = {clippedSegment.start, clippedSegment.end}},
469+
.blocksMovement = true,
470+
});
471+
}
403472
}
404473
}
405474
}
@@ -995,7 +1064,7 @@ bool pointInsideClosedBarrier(const FacilityLayout2D& layout, const Point2D& poi
9951064
}
9961065

9971066
bool pointHasClearance(const FacilityLayout2D& layout, const Point2D& point, double clearance) {
998-
if (!pointInAnyZone(layout, point) || pointInsideClosedBarrier(layout, point)) {
1067+
if ((!layout.zones.empty() && !pointInAnyZone(layout, point)) || pointInsideClosedBarrier(layout, point)) {
9991068
return false;
10001069
}
10011070

@@ -1371,18 +1440,29 @@ std::vector<Point2D> buildPath(const FacilityLayout2D& layout, const Point2D& st
13711440
return path;
13721441
}
13731442

1374-
Point2D constrainedMove(const FacilityLayout2D& layout, const Point2D& from, const Point2D& to) {
1375-
if (!movementCrossesBarrier(layout, from, to)) {
1443+
Point2D constrainedMove(const FacilityLayout2D& layout, const Point2D& from, const Point2D& to, double clearance) {
1444+
const auto effectiveClearance = std::max(0.0, clearance);
1445+
auto validDestination = [&](const Point2D& point) {
1446+
if (effectiveClearance > 0.0) {
1447+
return pointHasClearance(layout, point, effectiveClearance);
1448+
}
1449+
return (layout.zones.empty() || pointInAnyZone(layout, point)) && !pointInsideClosedBarrier(layout, point);
1450+
};
1451+
auto validMove = [&](const Point2D& candidate) {
1452+
return validDestination(candidate) && !movementCrossesBarrier(layout, from, candidate);
1453+
};
1454+
1455+
if (validMove(to)) {
13761456
return to;
13771457
}
13781458

13791459
const Point2D xOnly{.x = to.x, .y = from.y};
1380-
if (!movementCrossesBarrier(layout, from, xOnly)) {
1460+
if (validMove(xOnly)) {
13811461
return xOnly;
13821462
}
13831463

13841464
const Point2D yOnly{.x = from.x, .y = to.y};
1385-
if (!movementCrossesBarrier(layout, from, yOnly)) {
1465+
if (validMove(yOnly)) {
13861466
return yOnly;
13871467
}
13881468

@@ -1392,11 +1472,11 @@ Point2D constrainedMove(const FacilityLayout2D& layout, const Point2D& from, con
13921472
for (int i = 0; i < 8; ++i) {
13931473
const auto t = (low + high) * 0.5;
13941474
const auto candidate = from + ((to - from) * t);
1395-
if (movementCrossesBarrier(layout, from, candidate)) {
1396-
high = t;
1397-
} else {
1475+
if (validMove(candidate)) {
13981476
low = t;
13991477
best = candidate;
1478+
} else {
1479+
high = t;
14001480
}
14011481
}
14021482
return best;

src/domain/ScenarioSimulationInternal.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,6 @@ Point2D barrierSeparationVelocity(const FacilityLayout2D& layout, const Position
161161
bool movementCrossesBarrier(const FacilityLayout2D& layout, const Point2D& from, const Point2D& to);
162162
bool lineOfSightClear(const FacilityLayout2D& layout, const Point2D& from, const Point2D& to, double clearance);
163163
std::vector<Point2D> buildPath(const FacilityLayout2D& layout, const Point2D& start, const Point2D& goal, double clearance);
164-
Point2D constrainedMove(const FacilityLayout2D& layout, const Point2D& from, const Point2D& to);
164+
Point2D constrainedMove(const FacilityLayout2D& layout, const Point2D& from, const Point2D& to, double clearance = 0.0);
165165

166166
} // namespace safecrowd::domain::simulation_internal

0 commit comments

Comments
 (0)