/* ********************************************************************************** ** ** This file was created for the LibreCAD project (librecad.org), a 2D CAD program. ** ** Copyright (C) 2023 librecad (www.librecad.org) ** Copyright (C) 2023 dxli (github.com/dxli) ** ** This program is free software; you can redistribute it and/or ** modify it under the terms of the GNU General Public License ** as published by the Free Software Foundation; either version 2 ** of the License, or (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program; if not, write to the Free Software ** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ** ********************************************************************************** */ // File: lc_looputils.cpp #include #include #include #include #include #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" // ADDED: For LC_SplinePoints support #include "lc_parabola.h" // ADDED: For LC_Parabola support namespace { // Definition for buildLC_Loops (moved to namespace level for accessibility) /** * @brief Recursively builds an LC_Loops hierarchy from a container and all loops. * Clones atomic entities and traverses children via parent pointers. * @param cont The root container. * @param allLoops All loops for parent-child lookup. * @return The built LC_Loops tree. */ LC_LoopUtils::LC_Loops buildLoops(const RS_EntityContainer* cont, const std::vector>& allLoops) { auto loopCopy = std::make_shared(nullptr, true); for (RS_Entity* e : *cont) { if (e && !e->isContainer()) { loopCopy->addEntity(e->clone()); // Clone atomics for independent ownership } } 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; } /** * @brief Extends a line to cover the bounding box by computing intersections with box sides. * @param line The line to extend. * @param bbox The bounding rectangle. */ 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); } // Compare double with tolerance struct DoublePredicate { bool operator()(double a, double b) const { // If the absolute difference is within epsilon, consider them equal if (std::abs(a - b) <= RS_TOLERANCE * 100.) { return false; // They are considered equal, so neither is "less than" the other in a strict sense } return a < b; // Otherwise, use standard less-than comparison } }; // whether the entity is a single closed // arc//ellipticArc with angular length 0 is considered to be a whole circle/ellipse bool isClosed(const RS_Entity& en) { switch(en.rtti()) { case RS2::EntityCircle: return true; case RS2::EntityEllipse: { const RS_Ellipse& ellipse=static_cast(en); if (!ellipse.isArc()) return true; } [[fallthrough]]; case RS2::EntityArc: return en.getStartpoint() == en.getEndpoint(); case RS2::EntitySpline: { const LC_SplinePoints& spline = static_cast(en); return spline.isClosed(); } case RS2::EntityParabola: default: return false; } } /** * @brief PathBuilder is a utility class to build a QPainterPath by appending RS_Entity objects. * All appendages are transformed to UI coordinates using the provided RS_Painter. * Handles common entity types: Line, Arc, Circle, Ellipse, Spline, Parabola. * Supports moveTo for starting new subpaths and ensures continuity where possible. */ class PathBuilder { public: /** * @brief Constructor. * @param painter RS_Painter for UI coordinate transformations (required for UI mode). */ explicit PathBuilder(RS_Painter* painter = nullptr); /** * @brief Appends the given entity to the path, transforming to UI coordinates. * Handles startpoint continuity; moves to start if needed. * @param entity The RS_Entity to append. */ void append(RS_Entity* entity); /** * @brief Moves to the given position (in WCS; transformed to UI). * Starts a new subpath. * @param pos The position in WCS. */ void moveTo(const RS_Vector& pos); void lineTo(const RS_Vector& pos); /** * @brief Closes the current subpath. */ void closeSubpath(); /** * @brief Gets the built path. * @return Reference to the QPainterPath. */ QPainterPath& getPath() { return m_path; } const QPainterPath& getPath() const { return m_path; } /** * @brief Clears the path. */ void clear(); QPointF toGuiPoint(const RS_Vector& vp) const { RS_Vector guiVp = m_painter->toGui(vp); return {guiVp.x, guiVp.y}; } private: /** * @brief Appends a line in UI coordinates. * @param line The RS_Line. */ void appendLine(RS_Line* line); /** * @brief Appends an arc using cubic Bezier approximation in UI coordinates. * Follows entity direction (reversed or not). * @param arc The RS_Arc. */ void appendArc(RS_Arc* arc); /** * @brief Appends a circle as a full arc in UI coordinates. * @param circle The RS_Circle. */ void appendCircle(RS_Circle* circle); /** * @brief Appends an ellipse arc in UI coordinates using Bezier approximation. * @param ellipse The RS_Ellipse. */ void appendEllipse(RS_Ellipse* ellipse); /** * @brief Appends a spline (LC_SplinePoints) by appending quadratic Bezier segments in UI coordinates. * Assumes current position is at startpoint; uses quadTo for each quadratic segment. * @param spline The LC_SplinePoints. */ void appendSplinePoints(LC_SplinePoints* spline); /** * @brief Appends a parabola (LC_Parabola) by sampling stroke points in UI coordinates. * Assumes current position is at startpoint; uses lineTo for polyline approximation. * @param parabola The LC_Parabola. */ void appendParabola(LC_Parabola* parabola); RS_Painter* m_painter = nullptr; QPainterPath m_path; RS_Vector m_lastPoint{}; ///< Last point in WCS for continuity check. bool m_hasLastPoint = false; ///< Flag indicating if m_lastPoint is valid. }; 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(entity)); break; case RS2::EntityArc: appendArc(static_cast(entity)); break; case RS2::EntityCircle: appendCircle(static_cast(entity)); break; case RS2::EntityEllipse: appendEllipse(static_cast(entity)); break; case RS2::EntitySpline: appendSplinePoints(static_cast(entity)); break; case RS2::EntityParabola: appendParabola(static_cast(entity)); break; default: RS_DEBUG->print(RS_Debug::D_WARNING, "PathBuilder::append: Unsupported entity type %d", static_cast(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(); // Do not reset m_hasLastPoint; keep for potential next append } 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) { // TODO: need pixel level precision: issue #2035 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; // TODO: need pixel level precision: issue #2035 m_painter->drawEllipseBySplinePointsUI(*ellipse, m_path); } void PathBuilder::appendCircle(RS_Circle* circle) { if (!circle || !m_painter) return; // TODO: need pixel level precision: issue #2035 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) { // Degenerate case: connect to endpoint lineTo(spline->getEndpoint()); return; } // Assume current path position is at the startpoint of the first segment for (size_t i = 0; i < num_segs; ++i) { int seg_idx = static_cast(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 { // Fallback: line to end if quad points unavailable lineTo(end); } } } void PathBuilder::appendParabola(LC_Parabola* parabola) { if (!parabola) return; // Inherit from LC_SplinePoints for quadratic Bezier handling appendSplinePoints(parabola); } } namespace LC_LoopUtils { // Private implementation for LoopExtractor struct LoopExtractor::LoopData { std::vector unprocessed; ///< Remaining edges to process std::map processed; ///< Flag for processed status RS_Entity* current = nullptr; ///< Current entity in loop RS_Vector endPoint; ///< Current endpoint RS_Vector targetPoint; ///< Target start point for closure bool reversed = false; ///< Direction reversal flag }; /** * @brief Constructs LoopExtractor and initializes unprocessed edges. * Filters to atomic entities with length > ENDPOINT_TOLERANCE. */ LoopExtractor::LoopExtractor(const RS_EntityContainer& edges) : m_data(std::make_unique()) { for (RS_Entity* e : edges) { if (e->isAtomic() && e->getLength() > ENDPOINT_TOLERANCE) { // Skip degenerate zero-length edges m_data->unprocessed.push_back(e); m_data->processed[e] = false; } } } LoopExtractor::~LoopExtractor() = default; /** * @brief Extracts closed loops iteratively until no unprocessed edges remain. * Builds each loop by chaining connected edges, clones and orients for positive area. * @return Vector of unique_ptr to valid loop containers. */ std::vector> LoopExtractor::extract() { std::vector> results; while (!m_data->unprocessed.empty()) { m_loop = std::make_unique(); 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) { // Continue until closure within 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) { // Reverse direction for positive area (counter-clockwise) auto new_loop = std::make_unique(); for (int i = static_cast(m_loop->count()) - 1; i >= 0; --i) { RS_Entity* e = m_loop->entityAt(static_cast(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"); // Log invalid loops } } } return results; } /** * @brief Validates the current loop for closure. * @return True if valid. */ 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(e); return ell->getAngleLength() >= 2 * M_PI - RS_TOLERANCE; } if (type == RS2::EntitySpline) { // SUPPORT: Closed splines LC_SplinePoints* spl = static_cast(e); return spl->isClosed(); } if (type == RS2::EntityParabola) { // SUPPORT: Valid if primitives computed successfully LC_Parabola* para = static_cast(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; } /** * @brief Finds the first edge to start a loop. * @return Starting entity or nullptr. */ 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; } /** * @brief Finds and adds the next connected edge to the loop. * @return True if found. */ bool LoopExtractor::findNext() const { std::vector 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; } /** * @brief Gets entities connected to the current endpoint. * @return Vector of connected entities. */ std::vector LoopExtractor::getConnected() const { std::vector 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; } /** * @brief Selects the outermost (preferred direction) from connected edges. * @param edges Connected edges. * @return Selected entity. */ RS_Entity* LoopExtractor::findOutermost(std::vector edges) const { double radius = 1e-6; RS_Circle circle(nullptr, RS_CircleData(m_data->endPoint, radius)); double currentAngle = m_data->current->getDirection2(); // Incoming tangent at junction std::vector> angleDiffs; for (RS_Entity* e : edges) { // Compute outgoing tangent, accounting for potential reversal bool needsReversal = (e->getStartpoint().distanceTo(m_data->endPoint) > ENDPOINT_TOLERANCE); double outgoingAngle; if (!needsReversal) { outgoingAngle = e->getDirection1(); // Tangent at start } else { outgoingAngle = RS_Math::correctAngle(e->getDirection2() + M_PI); // Flip by pi for reversal } RS_VectorSolutions sol = RS_Information::getIntersection(&circle, e, true); if (!sol.hasValid()) continue; // Select the intersection closest to the outgoing direction 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; // Filter valid radius intersections double a = (v - m_data->endPoint).angle(); double adiff = RS_Math::correctAngle(a - outgoingAngle); if (adiff > M_PI) adiff = 2 * M_PI - adiff; // Minimal unsigned difference if (adiff < min_adiff) { min_adiff = adiff; selected_v = v; } } if (min_adiff > 1e-4) continue; // Skip if no close match (numerical or curvature issues) double angle = (selected_v - m_data->endPoint).angle(); double diff = RS_Math::correctAngle(angle - currentAngle); if (diff > M_PI) diff -= 2 * M_PI; // Signed difference in (-pi, pi] angleDiffs.push_back({diff, e}); } if (angleDiffs.empty()) return nullptr; // Sort by descending signed diff to prefer left turns (largest positive diff) for CCW outer boundary std::sort(angleDiffs.begin(), angleDiffs.end(), [](const auto& a, const auto& b) { return a.first > b.first; }); return angleDiffs[0].second; } // Private implementation for LoopSorter struct LoopSorter::Data { std::vector> loops; ///< Input loops std::shared_ptr> results; ///< Output hierarchy }; /** * @brief Predicate for sorting loops: ascending absolute area (small to large for parent assignment), tie-break by bounding box diagonal. * Ensures robust ordering for containment detection. */ 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; // Ascending abs area (small to large for parent assignment) } }; /** * @brief Constructs LoopSorter, filters degenerates, sorts, and builds hierarchy. */ LoopSorter::LoopSorter(std::vector> loops) : m_data(new Data) { m_data->loops = std::move(loops); sortAndBuild(); } LoopSorter::~LoopSorter() = default; /** * @brief Main sorting and hierarchy building: Filters zero-area, sorts ascending area, assigns parents small-to-large. * Builds forest of roots and converts to LC_Loops. */ void LoopSorter::sortAndBuild() { std::multimap orderedLoops; for (auto& p : m_data->loops) { p->setParent(nullptr); // Reset parents 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 forest; while (!orderedLoops.empty()) { RS_EntityContainer* child = orderedLoops.begin()->second; findParent(child, orderedLoops); // Assign immediate parent if (child->getParent() == nullptr) { forest.push_back(child); // Root if no parent } orderedLoops.erase(orderedLoops.begin()); } m_data->results = forestToLoops(forest); } /** * @brief Accessor for results. */ std::shared_ptr> LoopSorter::getResults() const { return m_data->results; } /** * @brief Converts forest roots to recursive LC_Loops trees using buildLoops. */ std::shared_ptr> LoopSorter::forestToLoops(std::vector forest) const { auto loops = std::make_shared>(); for (RS_EntityContainer* container: forest) { loops->push_back(buildLoops(container, m_data->loops)); // Build recursive hierarchy } return loops; } void LoopSorter::init() { // Placeholder for future initialization } /** * @brief Assigns the smallest enclosing parent using bbox inclusion and point-in-contour test. * Processes small-to-large to ensure immediate (direct) parent. */ void LoopSorter::findParent(RS_EntityContainer* loop, const std::multimap& 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; // Use bbox center for containment test for (auto it = sorted.begin(); ++it != sorted.end();) { // Iterate small to large auto* potentialParent = it->second; if (potentialParent == loop) continue; double parentArea = it->first; // Skip smaller or equal if (parentArea <= childArea + RS_TOLERANCE) continue; LC_Rect parentBox{potentialParent->getMin(), potentialParent->getMax()}; if (childBox.numCornersInside(parentBox) != 4) continue; // Quick bbox containment bool onContour = false; if (RS_Information::isPointInsideContour(testPoint, potentialParent, &onContour)) { loop->setParent(potentialParent); // Track hierarchy via parent pointer only 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); // Log orphan } // Private implementation for LoopOptimizer struct LoopOptimizer::Data { std::shared_ptr> results; ///< Processed results }; /** * @brief Constructs LoopOptimizer and processes the input contour. */ LoopOptimizer::LoopOptimizer(const RS_EntityContainer& contour) : m_data(new Data) { AddContainer(contour); } LoopOptimizer::~LoopOptimizer() = default; /** * @brief Accessor for results. */ std::shared_ptr> LoopOptimizer::GetResults() const { return m_data->results; } /** * @brief Processes a contour: Extract loops, sort, and build hierarchy. */ 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 implementation LC_Loops::LC_Loops(std::shared_ptr loop, bool ownsEntities) : m_loop(loop) { // Ownership managed via shared_ptr; autoDelete assumed true } LC_Loops::~LC_Loops() = default; /** * @brief Moves a child into the children vector. */ void LC_Loops::addChild(LC_Loops child) { m_children.push_back(std::move(child)); } /** * @brief Adds an entity to the outer loop container. */ void LC_Loops::addEntity(RS_Entity* entity) { m_loop->addEntity(entity); } /** * @brief Non-recursive check for point inside outer loop using winding rule. */ bool LC_Loops::isInsideOuter(const RS_Vector& point) const { bool onContour = false; return RS_Information::isPointInsideContour(point, m_loop.get(), &onContour); } /** * @brief Recursive inside check using odd-even parity on depth. */ bool LC_Loops::isInside(const RS_Vector& point) const { return getContainingDepth(point) % 2 == 1; } /** * @brief Computes recursive depth: 1 for outer + sum of children. * Uses outer-only check to avoid cycles. */ int LC_Loops::getContainingDepth(const RS_Vector& point) const { int depth = 0; if (isInsideOuter(point)) { // Check outer only depth = 1; for (const auto& child : m_children) { depth += child.getContainingDepth(point); } } return depth; } /** * @brief Builds hierarchical QPainterPath: Outer path (reversed if odd level), add children recursively. * Applies OddEvenFill for holes. */ 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; } /** * @brief Computes net area: Outer area minus children's total areas (subtracts holes, adds islands). */ 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(); // Recursive subtraction }); } /** * @brief Parametric equation for point on ellipse. */ 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; } /** * @brief First derivative (tangent vector) for ellipse. */ 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; } /** * @brief Appends an elliptic arc to path using cubic Bezier segments. * Segments based on sweep angle and aspect ratio for smoothness. * Optimized to reuse endpoint and tangent calculations across segments. */ 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(std::ceil(aspect - 1.0)); // More segments for high aspect double sweep = a2 - a1; int n = static_cast(std::ceil(std::abs(sweep) * 24. / M_PI )) + extra_segments; // Quadrants + extra if (n <= 0) return; // Degenerate case double dt = sweep / n; double lambda = (4.0 / 3.0) * std::tan(dt / 4.0); // Bezier control factor (constant) double current_t = a1; // Initial endpoint and tangent for first segment 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); // Reuse for next segment p0 = p3; prime0 = prime1; current_t = t1; } } /** * @brief Converts container entities to QPainterPath, handling lines, arcs, circles, ellipses, splines, and parabolas. * Closes the subpath; skips unsupported types. */ QPainterPath LC_Loops::buildPathFromLoop(RS_Painter* painter, const RS_EntityContainer& cont) const { PathBuilder builder{painter}; QPainterPath& path = builder.getPath(); if (cont.empty()) return path; // single closed entities may not have defined start/end points 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(); // avoid small gaps due to rounding errors if ((path.currentPosition() - builder.toGuiPoint({start.x, start.y})).manhattanLength() >= 3.) { LC_ERR<<__func__<<"(): added line at "<& loops) const { if (m_loop) loops.push_back(m_loop.get()); for (const auto& child : m_children) { child.getAllLoops(loops); } } /** * @brief Gets the bounding box from the outer loop. */ LC_Rect LC_Loops::getBoundingBox() const { if (m_loop == nullptr) return {}; m_loop->calculateBorders(); return {m_loop->getMin(), m_loop->getMax()}; } /** * @brief Alias for isInside (odd-even rule). */ bool LC_Loops::isPointInside(const RS_Vector& p) const { return getContainingDepth(p) % 2 == 1; } /** * @brief Flattens all atomic boundary entities from the hierarchy. */ std::vector LC_Loops::getAllBoundaries() const { std::vector loops; getAllLoops(loops); std::vector bounds; for (auto* l : loops) { for (RS_Entity* e : *l) { if (e->isAtomic()) bounds.push_back(e); } } return bounds; } /** * @brief Checks if entity represents a closed shape (e.g., full circle, zero-length line). */ 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(e); return std::abs(ell->getAngleLength() - 2 * M_PI) < RS_TOLERANCE_ANGLE; } if (type == RS2::EntityArc) { const RS_Arc* arc = static_cast(e); return std::abs(arc->getAngleLength() - 2 * M_PI) < RS_TOLERANCE_ANGLE; } if (type == RS2::EntitySpline) { // SUPPORT: Closed splines const LC_SplinePoints* spl = static_cast(e); return spl->isClosed(); } if (type == RS2::EntityParabola) { // SUPPORT: Parabolas are finite open arcs return false; } if (type == RS2::EntityLine) return e->getStartpoint() == e->getEndpoint(); return false; } /** * @brief Generates tile offsets for pattern repetition within the bounding box. * Filters tiles that intersect or contain points inside the loop. */ std::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) // Skip degenerate patterns return {}; RS_Vector offsetBase = bBox.lowerLeftCorner() - pBox.lowerLeftCorner(); std::vector tiles; // Use ceil for full coverage int nx = static_cast(std::ceil(bBox.width() / pWidth)) + 1; int ny = static_cast(std::ceil(bBox.height() / pHeight)) + 1; // Use pattern center for inside check 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}; // Quick bbox intersection if (!tileRect.intersects(bBox)) continue; // Include if overlaps or center inside if (overlap(tileRect) || isPointInside(tile + pCenter)) { tiles.push_back(tile); } } } return tiles; } /** * @brief Trims pattern entities to loop boundaries: Intersects, sorts params, creates subs inside loop. * Handles closed/open entities; skips odd intersections for closed. * For RS_Line: extends to bbox, dedups tiles by perpendicular intercept. */ std::unique_ptr LC_Loops::trimPatternEntities(const RS_Pattern& pattern) const { std::unique_ptr trimmed = std::make_unique(); std::vector tiles = createTiles(pattern); auto boundaries = getAllBoundaries(); std::map> 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(e->clone()); cloned->move(tile); // For RS_Line, extend the line to cover the whole contour, if the pattern line is seen for the first time // If the extended line is coincident with a previous one, skip it if (e->rtti() == RS2::EntityLine) { RS_Line* cline = static_cast(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& sset = savedIntercepts[e]; if (sset.find(key) != sset.end()) continue; sset.insert(key); // Extend to bbox extendLineToBBox(*cline, bBox); } std::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); } } // Remove duplicates 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 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(createSubEntity(cloned.get(), p1, p2)); // Filter short subs and add if inside 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(createSubEntity(cloned.get(), pw1, pw2)); if (sub && sub->getLength() > RS_TOLERANCE && isPointInside(sub->getMiddlePoint())) { sub->setVisible(true); trimmed->addEntity(sub.release()); } } } } } return trimmed; } /** * @brief Creates a trimmed sub-entity (line/arc/circle/ellipse/spline/parabola) between two points. * Handles type-specific parameterization. */ 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(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(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(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) { // IMPROVED: Use cut() for precise trimming LC_SplinePoints* spl = static_cast(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); // Ensure order LC_SplinePoints* seg1 = spl->cut(p1); // Trims original to start-p1 if (seg1) { LC_SplinePoints* sub = seg1->cut(p2); // Further trim p1-p2 if (sub && sub->getLength() > RS_TOLERANCE) return sub; delete seg1; // Cleanup } // Fallback: Resample splinePoints between nearest indices 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) { // IMPROVED: Exact tangents via FromEndPointsTangents LC_Parabola* para = static_cast(e); RS_Vector tan1 = para->getTangentDirection(p1).normalize(); RS_Vector tan2 = para->getTangentDirection(p2).normalize(); std::array ends = {p1, p2}; std::array tans = {tan1, tan2}; LC_ParabolaData sub_data = LC_ParabolaData::FromEndPointsTangents(ends, tans); if (sub_data.valid) { return new LC_Parabola(nullptr, sub_data); } // Fallback: Interpolate controls 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 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}; // Preserve y for parabolic shape } return new LC_Parabola(nullptr, LC_ParabolaData{sub_cps}); } return nullptr; } /** * @brief Sorts intersection points by parameterization along the entity (t for lines, angle for curves, arc-len/x-param for splines/parabolas). */ std::vector LC_Loops::sortPointsAlongEntity(RS_Entity* e, std::vector inters) const { std::vector> param_points; RS2::EntityType type = e->rtti(); if (type == RS2::EntityLine) { RS_Line* line = static_cast(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(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(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(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) { // IMPROVED: Binary search for arc-length param LC_SplinePoints* spl = static_cast(e); double total_len = spl->getLength(); if (total_len < RS_TOLERANCE) return {}; for (RS_Vector v : inters) { // Binary search approx for param t (normalized [0,1]) double low = 0.0, high = total_len; for (int iter = 0; iter < 10; ++iter) { // ~1e-3 precision double mid = (low + high) / 2.0; RS_Vector mid_pt = spl->getNearestDist(mid, spl->getStartpoint()); // Point at dist mid 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) { // IMPROVED: Exact x-param normalization LC_Parabola* para = static_cast(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; // [0,1] 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 sorted; for (auto& p : param_points) sorted.push_back(p.second); return sorted; } /** * @brief Recursive overlap check: Any entity bbox overlaps or child overlaps. */ 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); }); } } // namespace LC_LoopUtils