| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | #include <algorithm> |
| | #include <cmath> |
| | #include <set> |
| | #include <QPen> |
| | #include <QPainterPath> |
| |
|
| | #include "lc_looputils.h" |
| | #include "lc_rect.h" |
| | #include "rs_arc.h" |
| | #include "rs_circle.h" |
| | #include "rs_debug.h" |
| | #include "rs_ellipse.h" |
| | #include "rs_entitycontainer.h" |
| | #include "rs_information.h" |
| | #include "rs_line.h" |
| | #include "rs_math.h" |
| | #include "rs_painter.h" |
| | #include "rs_pattern.h" |
| | #include "rs_vector.h" |
| | #include "rs.h" |
| | #include "lc_splinepoints.h" |
| | #include "lc_parabola.h" |
| |
|
| | namespace { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | LC_LoopUtils::LC_Loops buildLoops(const RS_EntityContainer* cont, const std::vector<std::unique_ptr<RS_EntityContainer>>& allLoops) { |
| | auto loopCopy = std::make_shared<RS_EntityContainer>(nullptr, true); |
| | for (RS_Entity* e : *cont) { |
| | if (e && !e->isContainer()) { |
| | loopCopy->addEntity(e->clone()); |
| | } |
| | } |
| | LC_LoopUtils::LC_Loops lc(loopCopy, true); |
| | for (const auto& p : allLoops) { |
| | if (p.get()->getParent() == cont) { |
| | lc.addChild(buildLoops(p.get(), allLoops)); |
| | } |
| | } |
| | return lc; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | void extendLineToBBox(RS_Line& line, const LC_Rect& bbox) { |
| | const double maxSize = 1.5 * std::max(bbox.width(), bbox.height()); |
| | const RS_Vector offset = line.getTangentDirection({}).normalized() * maxSize; |
| |
|
| | line.setStartpoint(line.getStartpoint() - offset); |
| | line.setEndpoint(line.getEndpoint() + offset); |
| | } |
| |
|
| | |
| | struct DoublePredicate { |
| | bool operator()(double a, double b) const { |
| | |
| | if (std::abs(a - b) <= RS_TOLERANCE * 100.) { |
| | return false; |
| | } |
| | return a < b; |
| | } |
| | }; |
| |
|
| | |
| | |
| | bool isClosed(const RS_Entity& en) { |
| | switch(en.rtti()) { |
| | case RS2::EntityCircle: |
| | return true; |
| | case RS2::EntityEllipse: { |
| | const RS_Ellipse& ellipse=static_cast<const RS_Ellipse&>(en); |
| | if (!ellipse.isArc()) |
| | return true; |
| | } |
| | [[fallthrough]]; |
| | case RS2::EntityArc: |
| | return en.getStartpoint() == en.getEndpoint(); |
| | case RS2::EntitySpline: { |
| | const LC_SplinePoints& spline = static_cast<const LC_SplinePoints&>(en); |
| | return spline.isClosed(); |
| | } |
| | case RS2::EntityParabola: |
| | default: |
| | return false; |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | class PathBuilder { |
| | public: |
| | |
| | |
| | |
| | |
| | explicit PathBuilder(RS_Painter* painter = nullptr); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | void append(RS_Entity* entity); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | void moveTo(const RS_Vector& pos); |
| | void lineTo(const RS_Vector& pos); |
| |
|
| | |
| | |
| | |
| | void closeSubpath(); |
| |
|
| | |
| | |
| | |
| | |
| | QPainterPath& getPath() { return m_path; } |
| | const QPainterPath& getPath() const { return m_path; } |
| |
|
| | |
| | |
| | |
| | void clear(); |
| | QPointF toGuiPoint(const RS_Vector& vp) const |
| | { |
| | RS_Vector guiVp = m_painter->toGui(vp); |
| | return {guiVp.x, guiVp.y}; |
| | } |
| |
|
| | private: |
| | |
| | |
| | |
| | |
| | void appendLine(RS_Line* line); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | void appendArc(RS_Arc* arc); |
| |
|
| | |
| | |
| | |
| | |
| | void appendCircle(RS_Circle* circle); |
| |
|
| | |
| | |
| | |
| | |
| | void appendEllipse(RS_Ellipse* ellipse); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | void appendSplinePoints(LC_SplinePoints* spline); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | void appendParabola(LC_Parabola* parabola); |
| |
|
| | RS_Painter* m_painter = nullptr; |
| | QPainterPath m_path; |
| | RS_Vector m_lastPoint{}; |
| | bool m_hasLastPoint = false; |
| | }; |
| |
|
| | PathBuilder::PathBuilder(RS_Painter* painter) |
| | : m_painter(painter){ |
| | assert(m_painter != nullptr); |
| | m_path.setFillRule(Qt::OddEvenFill); |
| | m_hasLastPoint = false; |
| | } |
| |
|
| | void PathBuilder::append(RS_Entity* entity) { |
| | if (!entity || entity->isUndone()) return; |
| |
|
| | RS_Vector startp = entity->getStartpoint(); |
| | const double tol = 1e-6; |
| | if (!m_hasLastPoint || m_lastPoint.distanceTo(startp) > tol) { |
| | moveTo(startp); |
| | } |
| |
|
| | RS2::EntityType type = entity->rtti(); |
| |
|
| | switch (type) { |
| | case RS2::EntityLine: |
| | appendLine(static_cast<RS_Line*>(entity)); |
| | break; |
| | case RS2::EntityArc: |
| | appendArc(static_cast<RS_Arc*>(entity)); |
| | break; |
| | case RS2::EntityCircle: |
| | appendCircle(static_cast<RS_Circle*>(entity)); |
| | break; |
| | case RS2::EntityEllipse: |
| | appendEllipse(static_cast<RS_Ellipse*>(entity)); |
| | break; |
| | case RS2::EntitySpline: |
| | appendSplinePoints(static_cast<LC_SplinePoints*>(entity)); |
| | break; |
| | case RS2::EntityParabola: |
| | appendParabola(static_cast<LC_Parabola*>(entity)); |
| | break; |
| | default: |
| | RS_DEBUG->print(RS_Debug::D_WARNING, "PathBuilder::append: Unsupported entity type %d", static_cast<int>(type)); |
| | break; |
| | } |
| |
|
| | m_lastPoint = entity->getEndpoint(); |
| | m_hasLastPoint = true; |
| | } |
| |
|
| | void PathBuilder::moveTo(const RS_Vector& pos) { |
| | QPointF uiPos = toGuiPoint(pos); |
| | m_path.moveTo(uiPos); |
| | m_lastPoint = pos; |
| | m_hasLastPoint = true; |
| | } |
| |
|
| | void PathBuilder::lineTo(const RS_Vector& pos) { |
| | QPointF uiPos = toGuiPoint(pos); |
| | m_path.lineTo(uiPos); |
| | m_lastPoint = pos; |
| | m_hasLastPoint = true; |
| | } |
| |
|
| | void PathBuilder::closeSubpath() { |
| | m_path.closeSubpath(); |
| | |
| | } |
| |
|
| | void PathBuilder::clear() { |
| | m_path = QPainterPath(); |
| | m_lastPoint = RS_Vector(0., 0.); |
| | m_hasLastPoint = false; |
| | } |
| |
|
| | void PathBuilder::appendLine(RS_Line* line) { |
| | if (!line) |
| | return; |
| |
|
| | QPointF uiEnd = toGuiPoint(line->getEndpoint()); |
| |
|
| | m_path.lineTo(uiEnd); |
| | } |
| |
|
| | void PathBuilder::appendArc(RS_Arc* arc) { |
| | |
| | if (!arc || !m_painter) |
| | return; |
| |
|
| | double startAngle = arc->getAngle1(); |
| | double endAngle = arc->getAngle2(); |
| | if (arc->isReversed()) |
| | endAngle = startAngle - RS_Math::correctAngle(startAngle - endAngle); |
| | else |
| | endAngle = startAngle + RS_Math::correctAngle(endAngle - startAngle); |
| |
|
| | double startDeg = RS_Math::rad2deg(startAngle); |
| | double sweepDeg = RS_Math::rad2deg(endAngle - startAngle); |
| |
|
| | QPointF center = toGuiPoint(arc->getCenter()); |
| | double radiusX = m_painter->toGuiDX(arc->getRadius()); |
| | double radiusY = m_painter->toGuiDY(arc->getRadius()); |
| | QPointF halfSize{radiusX, radiusY}; |
| | QRectF arcRect{center - halfSize, center + halfSize}; |
| |
|
| | m_path.arcTo(arcRect, startDeg, sweepDeg); |
| | } |
| |
|
| | void PathBuilder::appendEllipse(RS_Ellipse* ellipse) { |
| | if (!ellipse || !m_painter) return; |
| |
|
| | |
| | m_painter->drawEllipseBySplinePointsUI(*ellipse, m_path); |
| | } |
| |
|
| | void PathBuilder::appendCircle(RS_Circle* circle) { |
| | if (!circle || !m_painter) return; |
| |
|
| | |
| | QPointF center = toGuiPoint(circle->getCenter()); |
| | double radiusX = m_painter->toGuiDX(circle->getRadius()); |
| | double radiusY = m_painter->toGuiDY(circle->getRadius()); |
| | QPointF halfSize{radiusX, radiusY}; |
| | QRectF circleRect{center - halfSize, center + halfSize}; |
| |
|
| | m_path.addEllipse(circleRect); |
| | } |
| |
|
| | void PathBuilder::appendSplinePoints(LC_SplinePoints* spline) { |
| | if (!spline || !m_painter) return; |
| |
|
| | const auto& points = spline->getPoints(); |
| | if (points.empty()) return; |
| |
|
| | size_t n_points = points.size(); |
| | size_t num_segs = spline->isClosed() ? n_points : n_points - 1; |
| | if (num_segs == 0) { |
| | |
| | lineTo(spline->getEndpoint()); |
| | return; |
| | } |
| |
|
| | |
| | for (size_t i = 0; i < num_segs; ++i) { |
| | int seg_idx = static_cast<int>(i); |
| | RS_Vector start, ctrl, end; |
| | if (spline->GetQuadPoints(seg_idx, &start, &ctrl, &end) != 0) { |
| | QPointF ui_ctrl = toGuiPoint(ctrl); |
| | QPointF ui_end = toGuiPoint(end); |
| | m_path.quadTo(ui_ctrl, ui_end); |
| | } else { |
| | |
| | lineTo(end); |
| | } |
| | } |
| | } |
| |
|
| | void PathBuilder::appendParabola(LC_Parabola* parabola) { |
| | if (!parabola) return; |
| |
|
| | |
| | appendSplinePoints(parabola); |
| | } |
| |
|
| | } |
| |
|
| | namespace LC_LoopUtils { |
| |
|
| | |
| | struct LoopExtractor::LoopData { |
| | std::vector<RS_Entity*> unprocessed; |
| | std::map<RS_Entity*, bool> processed; |
| | RS_Entity* current = nullptr; |
| | RS_Vector endPoint; |
| | RS_Vector targetPoint; |
| | bool reversed = false; |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | LoopExtractor::LoopExtractor(const RS_EntityContainer& edges) : |
| | m_data(std::make_unique<LoopData>()) |
| | { |
| | for (RS_Entity* e : edges) { |
| | if (e->isAtomic() && e->getLength() > ENDPOINT_TOLERANCE) { |
| | m_data->unprocessed.push_back(e); |
| | m_data->processed[e] = false; |
| | } |
| | } |
| | } |
| |
|
| | LoopExtractor::~LoopExtractor() = default; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | std::vector<std::unique_ptr<RS_EntityContainer>> LoopExtractor::extract() { |
| | std::vector<std::unique_ptr<RS_EntityContainer>> results; |
| | while (!m_data->unprocessed.empty()) { |
| | m_loop = std::make_unique<RS_EntityContainer>(); |
| | RS_Entity* first = findFirst(); |
| | if (first) { |
| | RS_Entity* cloned_first = first->clone(); |
| | m_loop->addEntity(cloned_first); |
| | m_data->processed[first] = true; |
| | m_data->current = cloned_first; |
| | RS_Vector start = cloned_first->getStartpoint(); |
| | RS_Vector end = cloned_first->getEndpoint(); |
| | m_data->targetPoint = start; |
| | m_data->endPoint = end; |
| | m_data->unprocessed.erase(std::remove(m_data->unprocessed.begin(), m_data->unprocessed.end(), first), m_data->unprocessed.end()); |
| | while (m_data->endPoint.distanceTo(m_data->targetPoint) > ENDPOINT_TOLERANCE) { |
| | if (findNext()) { |
| | if (m_data->endPoint.distanceTo(m_data->targetPoint) <= ENDPOINT_TOLERANCE) break; |
| | } else { |
| | break; |
| | } |
| | } |
| | if (validate()) { |
| | double area = m_loop->areaLineIntegral(); |
| | if (area < 0.0) { |
| | |
| | auto new_loop = std::make_unique<RS_EntityContainer>(); |
| | for (int i = static_cast<int>(m_loop->count()) - 1; i >= 0; --i) { |
| | RS_Entity* e = m_loop->entityAt(static_cast<unsigned>(i))->clone(); |
| | e->revertDirection(); |
| | new_loop->addEntity(e); |
| | } |
| | m_loop = std::move(new_loop); |
| | } |
| | results.push_back(std::move(m_loop)); |
| | } else { |
| | RS_DEBUG->print("LoopExtractor: Invalid loop discarded"); |
| | } |
| | } |
| | } |
| | return results; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | bool LoopExtractor::validate() const { |
| | if (m_loop->count() == 0) return false; |
| | if (m_loop->count() == 1) { |
| | RS_Entity* e = m_loop->entityAt(0); |
| | RS2::EntityType type = e->rtti(); |
| | if (type == RS2::EntityCircle) return true; |
| | if (type == RS2::EntityEllipse) { |
| | RS_Ellipse* ell = static_cast<RS_Ellipse*>(e); |
| | return ell->getAngleLength() >= 2 * M_PI - RS_TOLERANCE; |
| | } |
| | if (type == RS2::EntitySpline) { |
| | LC_SplinePoints* spl = static_cast<LC_SplinePoints*>(e); |
| | return spl->isClosed(); |
| | } |
| | if (type == RS2::EntityParabola) { |
| | LC_Parabola* para = static_cast<LC_Parabola*>(e); |
| | return para->getData().valid; |
| | } |
| | return e->getStartpoint().distanceTo(e->getEndpoint()) <= ENDPOINT_TOLERANCE; |
| | } |
| | RS_Entity* first = m_loop->entityAt(0); |
| | RS_Entity* last = m_loop->last(); |
| | return first->getStartpoint().distanceTo(last->getEndpoint()) <= ENDPOINT_TOLERANCE; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | RS_Entity* LoopExtractor::findFirst() const { |
| | if (m_data->unprocessed.empty()) return nullptr; |
| | RS_Entity* firstEdge = m_data->unprocessed[0]; |
| | RS_Vector mid = firstEdge->getMiddlePoint(); |
| | RS_Vector lineStart(mid.x - 1000, mid.y); |
| | RS_Line testLine(lineStart, mid); |
| | double minDist = RS_MAXDOUBLE; |
| | RS_Entity* closest = nullptr; |
| | for (RS_Entity* e : m_data->unprocessed) { |
| | RS_VectorSolutions sol = RS_Information::getIntersection(&testLine, e, true); |
| | if (sol.hasValid()) { |
| | for (RS_Vector v : sol) { |
| | double d = v.distanceTo(lineStart); |
| | if (d < minDist) { |
| | minDist = d; |
| | closest = e; |
| | } |
| | } |
| | } |
| | } |
| | return closest ? closest : firstEdge; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | bool LoopExtractor::findNext() const { |
| | std::vector<RS_Entity*> connected = getConnected(); |
| | if (connected.empty()) return false; |
| | RS_Entity* next = nullptr; |
| | if (connected.size() == 1) { |
| | next = connected[0]; |
| | } else { |
| | next = findOutermost(connected); |
| | } |
| | if (next) { |
| | RS_Entity* cloned = next->clone(); |
| | m_loop->addEntity(cloned); |
| | m_data->processed[next] = true; |
| | m_data->unprocessed.erase(std::remove(m_data->unprocessed.begin(), m_data->unprocessed.end(), next), m_data->unprocessed.end()); |
| | if (cloned->getStartpoint().distanceTo(m_data->endPoint) > ENDPOINT_TOLERANCE) { |
| | cloned->revertDirection(); |
| | } |
| | m_data->endPoint = cloned->getEndpoint(); |
| | m_data->current = cloned; |
| | return true; |
| | } |
| | return false; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | std::vector<RS_Entity*> LoopExtractor::getConnected() const { |
| | std::vector<RS_Entity*> ret; |
| | for (RS_Entity* e : m_data->unprocessed) { |
| | if (e->getStartpoint().distanceTo(m_data->endPoint) <= ENDPOINT_TOLERANCE || e->getEndpoint().distanceTo(m_data->endPoint) <= ENDPOINT_TOLERANCE) { |
| | ret.push_back(e); |
| | } |
| | } |
| | return ret; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | RS_Entity* LoopExtractor::findOutermost(std::vector<RS_Entity*> edges) const { |
| | double radius = 1e-6; |
| | RS_Circle circle(nullptr, RS_CircleData(m_data->endPoint, radius)); |
| | double currentAngle = m_data->current->getDirection2(); |
| | std::vector<std::pair<double, RS_Entity*>> angleDiffs; |
| | for (RS_Entity* e : edges) { |
| | |
| | bool needsReversal = (e->getStartpoint().distanceTo(m_data->endPoint) > ENDPOINT_TOLERANCE); |
| | double outgoingAngle; |
| | if (!needsReversal) { |
| | outgoingAngle = e->getDirection1(); |
| | } else { |
| | outgoingAngle = RS_Math::correctAngle(e->getDirection2() + M_PI); |
| | } |
| |
|
| | RS_VectorSolutions sol = RS_Information::getIntersection(&circle, e, true); |
| | if (!sol.hasValid()) continue; |
| |
|
| | |
| | RS_Vector selected_v; |
| | double min_adiff = RS_MAXDOUBLE; |
| | for (size_t k = 0; k < sol.getNumber(); ++k) { |
| | RS_Vector v = sol.get(k); |
| | double dist = v.distanceTo(m_data->endPoint); |
| | if (std::abs(dist - radius) > RS_TOLERANCE) continue; |
| |
|
| | double a = (v - m_data->endPoint).angle(); |
| | double adiff = RS_Math::correctAngle(a - outgoingAngle); |
| | if (adiff > M_PI) adiff = 2 * M_PI - adiff; |
| | if (adiff < min_adiff) { |
| | min_adiff = adiff; |
| | selected_v = v; |
| | } |
| | } |
| |
|
| | if (min_adiff > 1e-4) continue; |
| |
|
| | double angle = (selected_v - m_data->endPoint).angle(); |
| | double diff = RS_Math::correctAngle(angle - currentAngle); |
| | if (diff > M_PI) diff -= 2 * M_PI; |
| |
|
| | angleDiffs.push_back({diff, e}); |
| | } |
| |
|
| | if (angleDiffs.empty()) return nullptr; |
| | |
| | std::sort(angleDiffs.begin(), angleDiffs.end(), [](const auto& a, const auto& b) { |
| | return a.first > b.first; |
| | }); |
| | return angleDiffs[0].second; |
| | } |
| |
|
| | |
| | struct LoopSorter::Data { |
| | std::vector<std::unique_ptr<RS_EntityContainer>> loops; |
| | std::shared_ptr<std::vector<LC_Loops>> results; |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | struct LoopSorter::AreaPredicate { |
| | bool operator() (const RS_EntityContainer* a, const RS_EntityContainer* b) const { |
| | double areaA = std::abs(a->areaLineIntegral()); |
| | double areaB = std::abs(b->areaLineIntegral()); |
| | if (std::abs(areaA - areaB) < RS_TOLERANCE) { |
| | double diagA = (a->getMax() - a->getMin()).magnitude(); |
| | double diagB = (b->getMax() - b->getMin()).magnitude(); |
| | return diagA < diagB; |
| | } |
| | return areaA < areaB; |
| | } |
| | }; |
| |
|
| | |
| | |
| | |
| | LoopSorter::LoopSorter(std::vector<std::unique_ptr<RS_EntityContainer>> loops) : m_data(new Data) { |
| | m_data->loops = std::move(loops); |
| | sortAndBuild(); |
| | } |
| |
|
| | LoopSorter::~LoopSorter() = default; |
| |
|
| | |
| | |
| | |
| | |
| | void LoopSorter::sortAndBuild() { |
| | std::multimap<double, RS_EntityContainer*> orderedLoops; |
| | for (auto& p : m_data->loops) { |
| | p->setParent(nullptr); |
| | p->forcedCalculateBorders(); |
| | double area = std::abs(p->areaLineIntegral()); |
| | if (area < RS_TOLERANCE) { |
| | RS_DEBUG->print("LoopSorter: Skipping degenerate zero-area loop"); |
| | continue; |
| | } |
| | orderedLoops.emplace(area, p.get()); |
| | } |
| | std::vector<RS_EntityContainer*> forest; |
| | while (!orderedLoops.empty()) { |
| | RS_EntityContainer* child = orderedLoops.begin()->second; |
| | findParent(child, orderedLoops); |
| | if (child->getParent() == nullptr) { |
| | forest.push_back(child); |
| | } |
| | orderedLoops.erase(orderedLoops.begin()); |
| | } |
| | m_data->results = forestToLoops(forest); |
| | } |
| |
|
| | |
| | |
| | |
| | std::shared_ptr<std::vector<LC_Loops>> LoopSorter::getResults() const { |
| | return m_data->results; |
| | } |
| |
|
| | |
| | |
| | |
| | std::shared_ptr<std::vector<LC_Loops>> LoopSorter::forestToLoops(std::vector<RS_EntityContainer*> forest) const { |
| | auto loops = std::make_shared<std::vector<LC_Loops>>(); |
| | for (RS_EntityContainer* container: forest) { |
| | loops->push_back(buildLoops(container, m_data->loops)); |
| | } |
| | return loops; |
| | } |
| |
|
| | void LoopSorter::init() { |
| | |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | void LoopSorter::findParent(RS_EntityContainer* loop, const std::multimap<double, RS_EntityContainer*>& sorted) { |
| | if (sorted.size() == 1) |
| | return; |
| | LC_Rect childBox{loop->getMin(), loop->getMax()}; |
| | double childArea = std::abs(loop->areaLineIntegral()); |
| | RS_Vector testPoint = (loop->getMin() + loop->getMax()) / 2.0; |
| | for (auto it = sorted.begin(); ++it != sorted.end();) { |
| | auto* potentialParent = it->second; |
| | if (potentialParent == loop) |
| | continue; |
| | double parentArea = it->first; |
| |
|
| | |
| | if (parentArea <= childArea + RS_TOLERANCE) |
| | continue; |
| | LC_Rect parentBox{potentialParent->getMin(), potentialParent->getMax()}; |
| |
|
| | if (childBox.numCornersInside(parentBox) != 4) |
| | continue; |
| | bool onContour = false; |
| | if (RS_Information::isPointInsideContour(testPoint, potentialParent, &onContour)) { |
| | loop->setParent(potentialParent); |
| | RS_DEBUG->print("LoopSorter: Assigned parent for loop with area %f", childArea); |
| | return; |
| | } |
| | } |
| | RS_DEBUG->print("LoopSorter: No parent found for loop with area %f", childArea); |
| | } |
| |
|
| | |
| | struct LoopOptimizer::Data { |
| | std::shared_ptr<std::vector<LC_Loops>> results; |
| | }; |
| |
|
| | |
| | |
| | |
| | LoopOptimizer::LoopOptimizer(const RS_EntityContainer& contour) : m_data(new Data) { |
| | AddContainer(contour); |
| | } |
| |
|
| | LoopOptimizer::~LoopOptimizer() = default; |
| |
|
| | |
| | |
| | |
| | std::shared_ptr<std::vector<LC_Loops>> LoopOptimizer::GetResults() const { |
| | return m_data->results; |
| | } |
| |
|
| | |
| | |
| | |
| | void LoopOptimizer::AddContainer(const RS_EntityContainer& contour) { |
| | LoopExtractor extractor(contour); |
| | auto loops = extractor.extract(); |
| | LoopSorter sorter(std::move(loops)); |
| | m_data->results = sorter.getResults(); |
| | } |
| |
|
| | |
| |
|
| | LC_Loops::LC_Loops(std::shared_ptr<RS_EntityContainer> loop, bool ownsEntities) : m_loop(loop) { |
| | |
| | } |
| |
|
| | LC_Loops::~LC_Loops() = default; |
| |
|
| | |
| | |
| | |
| | void LC_Loops::addChild(LC_Loops child) { |
| | m_children.push_back(std::move(child)); |
| | } |
| |
|
| | |
| | |
| | |
| | void LC_Loops::addEntity(RS_Entity* entity) { |
| | m_loop->addEntity(entity); |
| | } |
| |
|
| | |
| | |
| | |
| | bool LC_Loops::isInsideOuter(const RS_Vector& point) const { |
| | bool onContour = false; |
| | return RS_Information::isPointInsideContour(point, m_loop.get(), &onContour); |
| | } |
| |
|
| | |
| | |
| | |
| | bool LC_Loops::isInside(const RS_Vector& point) const { |
| | return getContainingDepth(point) % 2 == 1; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | int LC_Loops::getContainingDepth(const RS_Vector& point) const { |
| | int depth = 0; |
| | if (isInsideOuter(point)) { |
| | depth = 1; |
| | for (const auto& child : m_children) { |
| | depth += child.getContainingDepth(point); |
| | } |
| | } |
| | return depth; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | QPainterPath LC_Loops::getPainterPath(RS_Painter* painter, int level) const { |
| | QPainterPath path = buildPathFromLoop(painter, *m_loop); |
| | for (const auto& child : m_children) { |
| | QPainterPath childPath = child.getPainterPath(painter, level + 1); |
| | path -= childPath; |
| | } |
| | path.setFillRule(Qt::OddEvenFill); |
| | return path; |
| | } |
| |
|
| | |
| | |
| | |
| | double LC_Loops::getTotalArea() const { |
| | return std::accumulate(m_children.begin(), m_children.end(), m_loop->areaLineIntegral(), [](double area, const LC_Loops& loop) { |
| | return area - loop.getTotalArea(); |
| | }); |
| | } |
| |
|
| | |
| | |
| | |
| | RS_Vector LC_Loops::e_point(const RS_Vector& center, double major, double minor, double rot, double t) const { |
| | RS_Vector local(major * std::cos(t), minor * std::sin(t)); |
| | local.rotate(rot); |
| | return center + local; |
| | } |
| |
|
| | |
| | |
| | |
| | RS_Vector LC_Loops::e_prime(double major, double minor, double rot, double t) const { |
| | RS_Vector local_prime(-major * std::sin(t), minor * std::cos(t)); |
| | local_prime.rotate(rot); |
| | return local_prime; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | void LC_Loops::addEllipticArc(QPainterPath& path, const RS_Vector& center, double major, double minor, double rot, double a1, double a2) const { |
| | double aspect = std::max(major, minor) / std::min(major, minor); |
| | int extra_segments = static_cast<int>(std::ceil(aspect - 1.0)); |
| | double sweep = a2 - a1; |
| | int n = static_cast<int>(std::ceil(std::abs(sweep) * 24. / M_PI )) + extra_segments; |
| | if (n <= 0) return; |
| | double dt = sweep / n; |
| | double lambda = (4.0 / 3.0) * std::tan(dt / 4.0); |
| | double current_t = a1; |
| |
|
| | |
| | RS_Vector p0 = e_point(center, major, minor, rot, current_t); |
| | RS_Vector prime0 = e_prime(major, minor, rot, current_t) * lambda; |
| |
|
| | if ((path.currentPosition() - QPointF{p0.x, p0.y}).manhattanLength() >= RS_TOLERANCE * 100.) { |
| | path.moveTo(p0.x, p0.y); |
| | } else { |
| | path.lineTo(p0.x, p0.y); |
| | } |
| |
|
| | for (int i = 0; i < n; ++i) { |
| | double t1 = current_t + dt; |
| | RS_Vector p3 = e_point(center, major, minor, rot, t1); |
| | RS_Vector prime1 = e_prime(major, minor, rot, t1) * lambda; |
| | RS_Vector p1 = p0 + prime0; |
| | RS_Vector p2 = p3 - prime1; |
| | path.cubicTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); |
| |
|
| | |
| | p0 = p3; |
| | prime0 = prime1; |
| | current_t = t1; |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | QPainterPath LC_Loops::buildPathFromLoop(RS_Painter* painter, const RS_EntityContainer& cont) const { |
| | PathBuilder builder{painter}; |
| | QPainterPath& path = builder.getPath(); |
| | if (cont.empty()) |
| | return path; |
| |
|
| | |
| | RS_Entity* first = cont.first(); |
| | if (isClosed(*first)) { |
| | builder.append(first); |
| | builder.closeSubpath(); |
| | return path; |
| | } |
| |
|
| | builder.moveTo(cont.first()->getStartpoint()); |
| | for (RS_Entity* e : cont) { |
| | if (e->isAtomic()) { |
| | RS_Vector start = e->getStartpoint(); |
| | |
| | if ((path.currentPosition() - builder.toGuiPoint({start.x, start.y})).manhattanLength() >= 3.) { |
| | LC_ERR<<__func__<<"(): added line at "<<start.x<<", "<<start.y; |
| | } |
| | builder.append(e); |
| | } |
| | } |
| | builder.closeSubpath(); |
| | return path; |
| | } |
| |
|
| | |
| | |
| | |
| | void LC_Loops::getAllLoops(std::vector<const RS_EntityContainer*>& loops) const { |
| | if (m_loop) loops.push_back(m_loop.get()); |
| | for (const auto& child : m_children) { |
| | child.getAllLoops(loops); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | LC_Rect LC_Loops::getBoundingBox() const { |
| | if (m_loop == nullptr) |
| | return {}; |
| | m_loop->calculateBorders(); |
| | return {m_loop->getMin(), m_loop->getMax()}; |
| | } |
| |
|
| | |
| | |
| | |
| | bool LC_Loops::isPointInside(const RS_Vector& p) const { |
| | return getContainingDepth(p) % 2 == 1; |
| | } |
| |
|
| | |
| | |
| | |
| | std::vector<RS_Entity*> LC_Loops::getAllBoundaries() const { |
| | std::vector<const RS_EntityContainer*> loops; |
| | getAllLoops(loops); |
| | std::vector<RS_Entity*> bounds; |
| | for (auto* l : loops) { |
| | for (RS_Entity* e : *l) { |
| | if (e->isAtomic()) bounds.push_back(e); |
| | } |
| | } |
| | return bounds; |
| | } |
| |
|
| | |
| | |
| | |
| | bool LC_Loops::isEntityClosed(const RS_Entity* e) const { |
| | RS2::EntityType type = e->rtti(); |
| | if (type == RS2::EntityCircle) return true; |
| | if (type == RS2::EntityEllipse) { |
| | const RS_Ellipse* ell = static_cast<const RS_Ellipse*>(e); |
| | return std::abs(ell->getAngleLength() - 2 * M_PI) < RS_TOLERANCE_ANGLE; |
| | } |
| | if (type == RS2::EntityArc) { |
| | const RS_Arc* arc = static_cast<const RS_Arc*>(e); |
| | return std::abs(arc->getAngleLength() - 2 * M_PI) < RS_TOLERANCE_ANGLE; |
| | } |
| | if (type == RS2::EntitySpline) { |
| | const LC_SplinePoints* spl = static_cast<const LC_SplinePoints*>(e); |
| | return spl->isClosed(); |
| | } |
| | if (type == RS2::EntityParabola) { |
| | return false; |
| | } |
| | if (type == RS2::EntityLine) return e->getStartpoint() == e->getEndpoint(); |
| | return false; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | std::vector<RS_Vector> LC_Loops::createTiles(const RS_Pattern& pattern) const { |
| | LC_Rect bBox = getBoundingBox(); |
| | LC_Rect pBox{pattern.getMin(), pattern.getMax()}; |
| | const double pWidth = pBox.width(); |
| | const double pHeight = pBox.height(); |
| | if (pWidth < 1e-6 || pHeight < 1e-6) |
| | return {}; |
| | RS_Vector offsetBase = bBox.lowerLeftCorner() - pBox.lowerLeftCorner(); |
| | std::vector<RS_Vector> tiles; |
| | |
| | int nx = static_cast<int>(std::ceil(bBox.width() / pWidth)) + 1; |
| | int ny = static_cast<int>(std::ceil(bBox.height() / pHeight)) + 1; |
| | |
| | RS_Vector pCenter = (pBox.lowerLeftCorner() + pBox.upperRightCorner()) / 2.0; |
| | for (int i = 0; i < nx; ++i) { |
| | for (int j = 0; j < ny; ++j) { |
| | RS_Vector tile = offsetBase + RS_Vector{pWidth * i, pHeight * j}; |
| | LC_Rect tileRect{pBox.lowerLeftCorner() + tile, pBox.upperRightCorner() + tile}; |
| | |
| | if (!tileRect.intersects(bBox)) |
| | continue; |
| | |
| | if (overlap(tileRect) || isPointInside(tile + pCenter)) { |
| | tiles.push_back(tile); |
| | } |
| | } |
| | } |
| | return tiles; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | std::unique_ptr<RS_EntityContainer> LC_Loops::trimPatternEntities(const RS_Pattern& pattern) const { |
| | std::unique_ptr<RS_EntityContainer> trimmed = std::make_unique<RS_EntityContainer>(); |
| | std::vector<RS_Vector> tiles = createTiles(pattern); |
| | auto boundaries = getAllBoundaries(); |
| | std::map<const RS_Entity*, std::set<double, DoublePredicate>> savedIntercepts; |
| | LC_Rect bBox = getBoundingBox(); |
| | for (const RS_Vector& tile : tiles) { |
| | for (RS_Entity* e : pattern) { |
| | if (!e->isAtomic()) continue; |
| | auto cloned = std::unique_ptr<RS_Entity>(e->clone()); |
| | cloned->move(tile); |
| |
|
| | |
| | |
| | if (e->rtti() == RS2::EntityLine) { |
| | RS_Line* cline = static_cast<RS_Line*>(cloned.get()); |
| | RS_Vector normal = cline->getNormalVector(); |
| | double intr = normal.dotP(cline->getStartpoint()); |
| | constexpr double TOL = 1e-6; |
| | double key = std::round(intr / TOL) * TOL; |
| | std::set<double, DoublePredicate>& sset = savedIntercepts[e]; |
| | if (sset.find(key) != sset.end()) |
| | continue; |
| | sset.insert(key); |
| | |
| | extendLineToBBox(*cline, bBox); |
| | } |
| | std::vector<RS_Vector> all_inters; |
| | for (auto* b : boundaries) { |
| | RS_VectorSolutions sol = RS_Information::getIntersection(cloned.get(), b, true); |
| | for (RS_Vector v : sol) { |
| | all_inters.push_back(v); |
| | } |
| | } |
| | |
| | std::sort(all_inters.begin(), all_inters.end(), [](const RS_Vector& a, const RS_Vector& b){ |
| | return a.x < b.x || (RS_Math::equal(a.x, b.x) && a.y < b.y); |
| | }); |
| | auto last = std::unique(all_inters.begin(), all_inters.end(), [](const RS_Vector& a, const RS_Vector& b){ |
| | return a.distanceTo(b) < RS_TOLERANCE; |
| | }); |
| | all_inters.erase(last, all_inters.end()); |
| | auto sorted_inters = sortPointsAlongEntity(cloned.get(), all_inters); |
| | bool closed = isEntityClosed(cloned.get()); |
| | if (sorted_inters.empty()) { |
| | if (isPointInside(cloned->getMiddlePoint())) { |
| | cloned->setVisible(true); |
| | trimmed->addEntity(cloned.release()); |
| | } |
| | continue; |
| | } |
| | if (closed && sorted_inters.size() % 2 != 0) { |
| | RS_DEBUG->print("LC_Loops::trimPatternEntities: Odd intersections for closed entity, skipping"); |
| | continue; |
| | } |
| | std::vector<RS_Vector> points; |
| | if (!closed) { |
| | points.push_back(cloned->getStartpoint()); |
| | points.insert(points.end(), sorted_inters.begin(), sorted_inters.end()); |
| | points.push_back(cloned->getEndpoint()); |
| | } else { |
| | points = sorted_inters; |
| | } |
| | for (size_t i = 0; i < points.size() - 1; ++i) { |
| | RS_Vector p1 = points[i]; |
| | RS_Vector p2 = points[i + 1]; |
| | if (p1.distanceTo(p2) < RS_TOLERANCE) |
| | continue; |
| | auto sub = std::unique_ptr<RS_Entity>(createSubEntity(cloned.get(), p1, p2)); |
| | |
| | if (sub && sub->getLength() > RS_TOLERANCE && isPointInside(sub->getMiddlePoint())) { |
| | sub->setVisible(true); |
| | trimmed->addEntity(sub.release()); |
| | } |
| | } |
| | if (closed && sorted_inters.size() > 0) { |
| | RS_Vector pw1 = points.back(); |
| | RS_Vector pw2 = points[0]; |
| | if (pw1.distanceTo(pw2) >= RS_TOLERANCE) { |
| | auto sub = std::unique_ptr<RS_Entity>(createSubEntity(cloned.get(), pw1, pw2)); |
| | if (sub && sub->getLength() > RS_TOLERANCE && isPointInside(sub->getMiddlePoint())) { |
| | sub->setVisible(true); |
| | trimmed->addEntity(sub.release()); |
| | } |
| | } |
| | } |
| | } |
| | } |
| | return trimmed; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | RS_Entity* LC_Loops::createSubEntity(RS_Entity* e, const RS_Vector& p1, const RS_Vector& p2) const { |
| | RS2::EntityType type = e->rtti(); |
| | if (type == RS2::EntityLine) { |
| | return new RS_Line(nullptr, RS_LineData(p1, p2)); |
| | } else if (type == RS2::EntityArc) { |
| | RS_Arc* arc = static_cast<RS_Arc*>(e); |
| | RS_Vector center = arc->getCenter(); |
| | double ang1 = (p1 - center).angle(); |
| | double ang2 = (p2 - center).angle(); |
| | return new RS_Arc(nullptr, RS_ArcData(center, arc->getRadius(), ang1, ang2, arc->isReversed())); |
| | } else if (type == RS2::EntityCircle) { |
| | RS_Circle* circle = static_cast<RS_Circle*>(e); |
| | RS_Vector center = circle->getCenter(); |
| | double ang1 = (p1 - center).angle(); |
| | double ang2 = (p2 - center).angle(); |
| | return new RS_Arc(nullptr, RS_ArcData(center, circle->getRadius(), ang1, ang2, false)); |
| | } else if (type == RS2::EntityEllipse) { |
| | RS_Ellipse* ell = static_cast<RS_Ellipse*>(e); |
| | RS_Vector center = ell->getCenter(); |
| | double rot = ell->getAngle(); |
| | RS_Vector lp1 = (p1 - center).rotate(-rot); |
| | double lang1 = std::atan2(lp1.y / ell->getMinorRadius(), lp1.x / ell->getMajorRadius()); |
| | RS_Vector lp2 = (p2 - center).rotate(-rot); |
| | double lang2 = std::atan2(lp2.y / ell->getMinorRadius(), lp2.x / ell->getMajorRadius()); |
| | return new RS_Ellipse(nullptr, {center, ell->getMajorP(), ell->getRatio(), lang1, lang2, ell->isReversed()}); |
| | } else if (type == RS2::EntitySpline) { |
| | LC_SplinePoints* spl = static_cast<LC_SplinePoints*>(e); |
| | double total_len = spl->getLength(); |
| | if (total_len < RS_TOLERANCE) return nullptr; |
| | double t1 = spl->getDistanceToPoint(p1) / total_len; |
| | double t2 = spl->getDistanceToPoint(p2) / total_len; |
| | if (t1 > t2) std::swap(t1, t2); |
| | LC_SplinePoints* seg1 = spl->cut(p1); |
| | if (seg1) { |
| | LC_SplinePoints* sub = seg1->cut(p2); |
| | if (sub && sub->getLength() > RS_TOLERANCE) return sub; |
| | delete seg1; |
| | } |
| | |
| | auto points = spl->getPoints(); |
| | size_t i1 = 0, i2 = points.size() - 1; |
| | double min_d1 = RS_MAXDOUBLE, min_d2 = RS_MAXDOUBLE; |
| | for (size_t i = 0; i < points.size(); ++i) { |
| | double d = points[i].distanceTo(p1); |
| | if (d < min_d1) { min_d1 = d; i1 = i; } |
| | d = points[i].distanceTo(p2); |
| | if (d < min_d2) { min_d2 = d; i2 = i; } |
| | } |
| | if (i1 > i2) std::swap(i1, i2); |
| | LC_SplinePointsData sub_data; |
| | sub_data.splinePoints.assign(points.begin() + i1, points.begin() + i2 + 1); |
| | return new LC_SplinePoints(nullptr, sub_data); |
| | } else if (type == RS2::EntityParabola) { |
| | LC_Parabola* para = static_cast<LC_Parabola*>(e); |
| | RS_Vector tan1 = para->getTangentDirection(p1).normalize(); |
| | RS_Vector tan2 = para->getTangentDirection(p2).normalize(); |
| | std::array<RS_Vector, 2> ends = {p1, p2}; |
| | std::array<RS_Vector, 2> tans = {tan1, tan2}; |
| | LC_ParabolaData sub_data = LC_ParabolaData::FromEndPointsTangents(ends, tans); |
| | if (sub_data.valid) { |
| | return new LC_Parabola(nullptr, sub_data); |
| | } |
| | |
| | auto cps = para->getData().controlPoints; |
| | std::sort(cps.begin(), cps.end(), [](const RS_Vector& a, const RS_Vector& b){ return a.x < b.x; }); |
| | double x1 = p1.x, x2 = p2.x; |
| | if (x1 > x2) std::swap(x1, x2); |
| | std::array<RS_Vector, 3> sub_cps; |
| | double range = x2 - x1; |
| | if (range < RS_TOLERANCE) return nullptr; |
| | for (int i = 0; i < 3; ++i) { |
| | double ratio = (cps[i].x - x1) / range; |
| | if (ratio < 0) ratio = 0; else if (ratio > 1) ratio = 1; |
| | sub_cps[i] = RS_Vector{x1 + ratio * range, cps[i].y}; |
| | } |
| | return new LC_Parabola(nullptr, LC_ParabolaData{sub_cps}); |
| | } |
| | return nullptr; |
| | } |
| |
|
| | |
| | |
| | |
| | std::vector<RS_Vector> LC_Loops::sortPointsAlongEntity(RS_Entity* e, std::vector<RS_Vector> inters) const { |
| | std::vector<std::pair<double, RS_Vector>> param_points; |
| | RS2::EntityType type = e->rtti(); |
| | if (type == RS2::EntityLine) { |
| | RS_Line* line = static_cast<RS_Line*>(e); |
| | RS_Vector start = line->getStartpoint(); |
| | RS_Vector dir = line->getEndpoint() - start; |
| | double len = dir.magnitude(); |
| | if (len < RS_TOLERANCE) return {}; |
| | RS_Vector unit = dir / len; |
| | for (RS_Vector v : inters) { |
| | double t = (v - start).dotP(unit); |
| | if (t >= 0 - RS_TOLERANCE && t <= len + RS_TOLERANCE) param_points.emplace_back(t, v); |
| | } |
| | } else if (type == RS2::EntityArc) { |
| | RS_Arc* arc = static_cast<RS_Arc*>(e); |
| | RS_Vector center = arc->getCenter(); |
| | double a1 = arc->getAngle1(); |
| | bool reversed = arc->isReversed(); |
| | for (RS_Vector v : inters) { |
| | double ang = (v - center).angle(); |
| | double diff = RS_Math::getAngleDifference(a1, ang, reversed); |
| | param_points.emplace_back(diff, v); |
| | } |
| | } else if (type == RS2::EntityCircle) { |
| | RS_Circle* circle = static_cast<RS_Circle*>(e); |
| | RS_Vector center = circle->getCenter(); |
| | if (!inters.empty()) { |
| | double ref_ang = (inters[0] - center).angle(); |
| | for (RS_Vector v : inters) { |
| | double ang = (v - center).angle(); |
| | double diff = RS_Math::correctAngle(ang - ref_ang); |
| | param_points.emplace_back(diff, v); |
| | } |
| | } |
| | } else if (type == RS2::EntityEllipse) { |
| | RS_Ellipse* ell = static_cast<RS_Ellipse*>(e); |
| | RS_Vector center = ell->getCenter(); |
| | double rot = ell->getAngle(); |
| | double a1 = ell->getAngle1(); |
| | bool reversed = ell->isReversed(); |
| | for (RS_Vector v : inters) { |
| | RS_Vector local = (v - center).rotate(-rot); |
| | double ang = std::atan2(local.y / ell->getMinorRadius(), local.x / ell->getMajorRadius()); |
| | double diff = RS_Math::getAngleDifference(a1, ang, reversed); |
| | param_points.emplace_back(diff, v); |
| | } |
| | } else if (type == RS2::EntitySpline) { |
| | LC_SplinePoints* spl = static_cast<LC_SplinePoints*>(e); |
| | double total_len = spl->getLength(); |
| | if (total_len < RS_TOLERANCE) return {}; |
| | for (RS_Vector v : inters) { |
| | |
| | double low = 0.0, high = total_len; |
| | for (int iter = 0; iter < 10; ++iter) { |
| | double mid = (low + high) / 2.0; |
| | RS_Vector mid_pt = spl->getNearestDist(mid, spl->getStartpoint()); |
| | double mid_dist_to_v = mid_pt.distanceTo(v); |
| | if (mid_dist_to_v < RS_TOLERANCE) break; |
| | if (mid < total_len / 2) high = mid; else low = mid; |
| | } |
| | double dist_along = (low + high) / 2.0; |
| | param_points.emplace_back(dist_along / total_len, v); |
| | } |
| | } else if (type == RS2::EntityParabola) { |
| | LC_Parabola* para = static_cast<LC_Parabola*>(e); |
| | LC_ParabolaData& d = para->getData(); |
| | if (!d.valid) return {}; |
| | double x_min = std::min({d.controlPoints[0].x, d.controlPoints[1].x, d.controlPoints[2].x}); |
| | double x_max = std::max({d.controlPoints[0].x, d.controlPoints[1].x, d.controlPoints[2].x}); |
| | double x_range = x_max - x_min; |
| | if (x_range < RS_TOLERANCE) return {}; |
| | for (RS_Vector v : inters) { |
| | double x_param = d.FindX(v); |
| | double norm_param = (x_param - x_min) / x_range; |
| | param_points.emplace_back(norm_param, v); |
| | } |
| | } |
| | std::sort(param_points.begin(), param_points.end(), [](const auto& a, const auto& b){ |
| | return a.first < b.first; |
| | }); |
| | std::vector<RS_Vector> sorted; |
| | for (auto& p : param_points) sorted.push_back(p.second); |
| | return sorted; |
| | } |
| |
|
| | |
| | |
| | |
| | bool LC_Loops::overlap(const LC_Rect& other) const { |
| | const bool ret = std::any_of(m_loop->begin(), m_loop->end(), [&other](const RS_Entity* entity) { |
| | return entity != nullptr && other.overlaps(LC_Rect{entity->getMin(), entity->getMax()}); |
| | }); |
| | return ret || std::any_of(m_children.cbegin(), m_children.cend(), [&other](const LC_Loops& loop) { |
| | return loop.overlap(other); |
| | }); |
| | } |
| | } |
| |
|