@@ -449,6 +449,12 @@ QPointF entryOutsideSample(const QRectF& rectangle, safecrowd::domain::StairEntr
449449std::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
453459bool 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+
473496std::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+
717812double 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 )) {
0 commit comments