/**************************************************************************** ** Various utility computation methods Copyright (C) 2024 LibreCAD.org Copyright (C) 2024 sand1024 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. **********************************************************************/ #include #include #include "lc_linemath.h" #include "rs.h" #include "rs_information.h" #include "rs_line.h" #include "rs_math.h" #include "rs_vector.h" namespace { // Compare by coordinates // points within RS_TOLERANCE distance are considered equal bool compareCoordinates(const RS_Vector& p0, const RS_Vector& p1) { return p0.distanceTo(p1) >= RS_TOLERANCE && ( p0.x < p1.x || (p0.x <= p1.x && p0.y < p1.y)); } /** * @brief isCounterclockwise v3 - v1, v2 - v1, if ordered as counterclockwise * @param v1 * @param v2 * @param v3 * @return */ bool isCounterClockwise (const RS_Vector& v1, const RS_Vector& v2, const RS_Vector& v3) { double res = RS_Vector::crossP((v2 - v1).normalized(), (v3 - v1).normalized()).z; return res >= RS_TOLERANCE; } } /** * Calculates point located on specified distance and angle from given starting point * @param startPoint start point * @param angleValueDegree angle in degrees * @param distance distance for vector * @return resulting point */ RS_Vector LC_LineMath::getEndOfLineSegment(const RS_Vector &startPoint, double angleValueDegree, double distance){ double angle = RS_Math::deg2rad(angleValueDegree); RS_Vector line = RS_Vector::polar(distance, angle); RS_Vector result = startPoint + line; return result; } /** * Return points that is located by specified distance and angle from given start point * @param startPoint start point * @param angleValueRad angle to target end point from start point (in radians) * @param distance distance between start and end point * @return resulting point */ RS_Vector LC_LineMath::relativePoint(const RS_Vector &startPoint, double distance, double angleValueRad){ RS_Vector offsetVector = RS_Vector::polar(distance, angleValueRad); RS_Vector result = startPoint + offsetVector; return result; } RS_Vector LC_LineMath::calculateAngleSegment(const RS_Vector& startPoint, const RS_Vector& previousLineStart, const RS_Vector& previousLineEnd, double angleValueDegree, bool angleRelative, double distance) { double angle = RS_Math::deg2rad(angleValueDegree); double realAngle = defineActualSegmentAngle(angle, angleRelative, previousLineStart, previousLineEnd); RS_Vector line = RS_Vector::polar(distance, realAngle); RS_Vector result = startPoint + line; return result; } /** * Defines actual angle to provide line segment. If angle value is relative, adds that angle to own angle of line segment * @param angle specified angle * @param angleIsRelative if true, angle value is related * @param previousLineStart previous line segment start * @param previousLineEnd previous line segment end * @return */ double LC_LineMath::defineActualSegmentAngle(double angle, bool angleIsRelative, const RS_Vector& previousLineStart, const RS_Vector& previousLineEnd) { double result = angle; if (angleIsRelative) { RS_Vector line = previousLineEnd - previousLineStart; double previousSegmentAngle = line.angle(); result = angle + previousSegmentAngle; } return result; } /** * Calculates projection of given point to infinite line (ray), that extends lines provided by coordinates * @param coord point coordinates * @param lineStartPoint line start * @param lineEndPoint line end * @return projectsion coordinates */ RS_Vector LC_LineMath::getNearestPointOnInfiniteLine(const RS_Vector& coord, const RS_Vector& lineStartPoint, const RS_Vector& lineEndPoint) { RS_Vector ae = lineEndPoint - lineStartPoint; RS_Vector ea = lineStartPoint - lineEndPoint; RS_Vector ap = coord - lineStartPoint; double magnitude = ae.magnitude(); if (magnitude < RS_TOLERANCE || ea.magnitude() < RS_TOLERANCE) { return RS_Vector(false); } // Orthogonal projection from both sides: RS_Vector ba = ae * RS_Vector::dotP(ae, ap) / (magnitude * magnitude); return lineStartPoint + ba; } /** * Calculates actual endpoint of line segment, that starts from given point by provided angle and is controlled by provided * snap point. Actually, the endpoint of segment is projection of snap point to infinite line that has specified angle and starts * from given starting point. * @param angleValueDegree angle value in degrees * @param startPoint start point of line segment * @param toSnapPoint snap point * @return end point for the segment */ RS_Vector LC_LineMath::calculateEndpointForAngleDirection(double wcsAngleRad, const RS_Vector& startPoint, const RS_Vector& toSnapPoint) { RS_Vector possibleEndPoint; RS_Vector infiniteTickStartPoint = startPoint; RS_Vector infiniteTickEndPoint = infiniteTickStartPoint.relative(10.0, wcsAngleRad); RS_Vector pointOnInfiniteTick = getNearestPointOnInfiniteLine(toSnapPoint, infiniteTickStartPoint, infiniteTickEndPoint); possibleEndPoint = pointOnInfiniteTick; return possibleEndPoint; } RS_Vector LC_LineMath::calculateEndpointForAngleDirection(double angleValueDegree, bool angleIsRelative, const RS_Vector& fromPoint, const RS_Vector& toSnapPoint, const RS_Vector& previousLineStart, const RS_Vector& previousLineEnd) { double angle = RS_Math::deg2rad(angleValueDegree); RS_Vector infiniteTickStartPoint = fromPoint; double realAngle = defineActualSegmentAngle(angle, angleIsRelative, previousLineStart, previousLineEnd); RS_Vector infiniteTickVector = RS_Vector::polar(10.0, realAngle); RS_Vector infiniteTickEndPoint = infiniteTickStartPoint + infiniteTickVector; RS_Vector pointOnInfiniteTick = getNearestPointOnInfiniteLine(toSnapPoint, infiniteTickStartPoint, infiniteTickEndPoint); return pointOnInfiniteTick; } /** * Returns nearest projection of point to given line * @param line line * @param coord point * @param infiniteLine if true, given line is considered to be infinite * @return point */ RS_Vector LC_LineMath::getNearestPointOnLine(const RS_Line* line, const RS_Vector& coord, bool infiniteLine){ // For infinite lines, find the nearest point is not limited by start/end points bool onEntity = ! infiniteLine; return line != nullptr ? line->getNearestPointOnEntity(coord, onEntity, nullptr) : RS_Vector{false}; } /** * Determines point that lies on circle of provided center and radius and with specific angle to point * @param radius circle radius * @param arcAngle circle angle * @param centerCircle center of circle * @return point coordinates */ RS_Vector LC_LineMath::findPointOnCircle(double radius, double arcAngle, const RS_Vector& centerCircle){ RS_Vector radiusVector = RS_Vector::polar(radius, arcAngle); RS_Vector pointPos = centerCircle + radiusVector; return pointPos; } /** * Determines how given point relates to provided line * @param startPos start point of line * @param endPos end point of line * @param point point to check * @return point position */ int LC_LineMath::getPointPosition(const RS_Vector &startPos, const RS_Vector &endPos, const RS_Vector &point){ RS_Vector a = endPos - startPos; // 1 RS_Vector b = point - startPos; // 2 double sa = a. x * b.y - b.x * a.y; // 3 if (sa > 0.0) { return LEFT; } if (sa < 0.0) { return RIGHT; } if ((a.x * b.x < 0.0) || (a.y * b.y < 0.0)) { return BEHIND; } if (a.magnitude() < b.magnitude()) { return BEYOND; } if (startPos == point) { return ORIGIN; } if (endPos == point) { return DESTINATION; } return BETWEEN; } /** * Checks whether two lines defined by their edge points lay on the same ray vector * @param line1Start * @param line1End * @param line2Start * @param line2End * @return */ bool LC_LineMath::areLinesOnSameRay(const RS_Vector& line1Start, const RS_Vector& line1End, const RS_Vector& line2Start, const RS_Vector& line2End) { // TODO: create a generic algorithm to find whether points are collinear // Find the rank of the matrix formed by point homogeneous coordinates: // coincidence: rank = 1 // collinear: rank = 2 // coplanar: rank = 3 double angle1 = line1Start.angleTo(line1End); double angle2 = line1Start.angleTo(line2End); double angle3 = line1Start.angleTo(line2Start); // if all points are on the same ray, the angles from first point to remaining 3 points will be the same angle1 = RS_Math::correctAngle0ToPi(angle1); angle2 = RS_Math::correctAngle0ToPi(angle2); angle3 = RS_Math::correctAngle0ToPi(angle3); bool sameLine = false; if (std::abs(angle1 - angle2) < RS_TOLERANCE_ANGLE && std::abs(angle1 - angle3) < RS_TOLERANCE_ANGLE) { sameLine = true; } return sameLine; } /** * Check whether distance between points is meaningful * @param startPoint * @param endPoint * @return */ bool LC_LineMath::isNonZeroLineLength(const RS_Vector &startPoint, const RS_Vector &endPoint){ return (endPoint - startPoint).squared() > RS_TOLERANCE; } /** * Returns meaningful value. If provided candidate is not meaningful, returns replacement value * @param candidate * @param replacementValue * @return */ double LC_LineMath::getMeaningful(double candidate, double replacementValue){ return (std::abs(candidate) < RS_TOLERANCE) ? replacementValue : candidate; } double LC_LineMath::getMeaningfulPositive(double candidate, double replacementValue){ return (candidate < RS_TOLERANCE) ? replacementValue : candidate; } /** * Checks and returns meaningful angle - if provided value is not meaningful, returns replacement value * @param candidate * @param replacementValue * @return */ double LC_LineMath::getMeaningfulAngle(double candidate, double replacementValue){ return (std::abs(candidate) < RS_TOLERANCE_ANGLE) ? replacementValue : candidate; } /** * Returns true if value is meaningful * @param value * @return */ bool LC_LineMath::isMeaningful(double value){ return std::abs(value) >= RS_TOLERANCE; } /** * Return true if angle value is not meaningful * @param value * @return */ bool LC_LineMath::isNotMeaningful(double value){ return std::abs(value) < RS_TOLERANCE; } /** * Returns true if angle value is meaningful * @param value * @return */ bool LC_LineMath::isMeaningfulAngle(double value){ return std::abs(value) >= RS_TOLERANCE_ANGLE; } bool LC_LineMath::isSameAngle(double angle1, double angle2) { return std::abs(angle1 - angle2) < RS_TOLERANCE_ANGLE; } /** * Return true if distance between two points is meaningful and so these are * different points * @param v1 first point * @param v2 second point * @return true if points are different */ bool LC_LineMath::isMeaningfulDistance(const RS_Vector &v1, const RS_Vector &v2){ double distance = v1.distanceTo(v2); return distance >= RS_TOLERANCE; } /** * Return true if distance between two points is not meaningful (close to zero * and less than RS_TOLERANCE) - so basically these to points may be considered as * the same point * @param v1 first point * @param v2 second point * @return true if distance not meaningful, false otherwise */ bool LC_LineMath::isNotMeaningfulDistance(const RS_Vector &v1, const RS_Vector &v2){ double distance = v1.distanceTo(v2); return distance < RS_TOLERANCE; } /** * Creates line that is parallel to given original line (defined by start and and point), that is located * on given distance * @param start start of original line * @param end end of original line * @param distance distance for parallel line * @return line data that describes parallel line */ RS_LineData LC_LineMath::createParallel(const RS_Vector &start, const RS_Vector &end, double distance){ RS_Line line{nullptr, {start, end}}; double ang = line.getDirection1() + M_PI_2; // calculate 1st parallel: line.move(RS_Vector::polar(distance, ang)); return line.getData(); } /** * Determines intersection point for two lines defined by provided start and end vectors * @param s1 start of line 1 * @param e1 end of line 1 * @param s2 start of line 2 * @param e2 end of line 2 * @return intersection point, invalid vector if no intersection point found */ RS_Vector LC_LineMath::getIntersectionLineLine(const RS_Vector& s1, const RS_Vector& e1, const RS_Vector& s2, const RS_Vector& e2) { RS_Line line1{nullptr, {s1, e1}}; RS_Line line2{nullptr, {s2, e2}}; RS_VectorSolutions sol = RS_Information::getIntersectionLineLine(&line1, &line2); return sol.empty() ? RS_Vector{false} : sol.at(0); } RS_Vector LC_LineMath::getIntersectionLineLineFast(const RS_Vector& s1, const RS_Vector& e1, const RS_Vector& s2, const RS_Vector& e2) { RS_Vector ret; double num = ((e2.x - s2.x) * (s1.y - s2.y) - (e2.y - s2.y) * (s1.x - s2.x)); double div = ((e2.y - s2.y) * (e1.x - s1.x) - (e2.x - s2.x) * (e1.y - s1.y)); double angle1 = s1.angleTo(e1); double angle2 = s2.angleTo(e2); if (fabs(div)>RS_TOLERANCE && fabs(remainder(angle1-angle2, M_PI))>=RS_TOLERANCE*10.) { double u = num / div; double xs = s1.x + u * (e1.x - s1.x); double ys = s1.y + u * (e1.y - s1.y); ret = RS_Vector(xs, ys); } else { // lines are parallel ret = RS_Vector(false); } return ret; } RS_Vector LC_LineMath::getIntersectionInfiniteLineLineFast(const RS_Vector& s1, const RS_Vector& e1, const RS_Vector& s2, const RS_Vector& e2, double offsetX, double offsetY) { RS_Vector ret; double num = ((e2.x - s2.x) * (s1.y - s2.y) - (e2.y - s2.y) * (s1.x - s2.x)); double div = ((e2.y - s2.y) * (e1.x - s1.x) - (e2.x - s2.x) * (e1.y - s1.y)); double angle1 = s1.angleTo(e1); double angle2 = s2.angleTo(e2); if (fabs(div)>RS_TOLERANCE && fabs(remainder(angle1-angle2, M_PI))>=RS_TOLERANCE*10.) { double u = num / div; double xs = s1.x + u * (e1.x - s1.x); double ys = s1.y + u * (e1.y - s1.y); // check that intersection is within finite line, here we expect that start/end is properly orderred (from min to max) if ((xs >= (s2.x-offsetX)) && (xs <= (e2.x+offsetX)) && (ys >= (s2.y-offsetY)) && (ys <= (e2.y+offsetY))){ ret = RS_Vector(xs, ys); } else { ret = RS_Vector(false); } } else { // lines are parallel ret = RS_Vector(false); } return ret; } bool LC_LineMath::hasIntersectionLineRect(const RS_Vector& lineStart, const RS_Vector& lineEnd, const RS_Vector& rectMin, const RS_Vector& rectMax) { RS_Vector direction = lineEnd - lineStart; // fixme - rewrite to faster implementation as it is used in rendering pipeline!! // here we check intersection of infinite line and two diagonals of given rect with edges parallel to axis. if (hasLineIntersection(lineStart, direction, rectMin, rectMax)){ // intersection with first diagonal return true; } else{ return hasLineIntersection(lineStart, direction, {rectMin.x, rectMax.y}, {rectMax.x, rectMin.y}); // intersection with second diagnonal } } /* * check whether there is intersection of infinite line and segment */ bool LC_LineMath::hasLineIntersection(RS_Vector p0, RS_Vector direction, RS_Vector p2, RS_Vector p3) { RS_Vector P = p2; RS_Vector R = p3 - p2; RS_Vector Q = p0; RS_Vector S = direction; RS_Vector N = RS_Vector(S.y, -S.x); // float t = dot(Q-P, N) / dot(R, N); RS_Vector tmp = Q-P; double t = tmp.dotP(N) / R.dotP(N); if (t >= 0.0 && t <= 1.0){ return true; // return P + R * t; } return false; // return RS_Vector(-1.0); } /** * @brief convexHull - find the convex hull by Graham's scan * @param points - input points * @return - the convex hull found */ RS_VectorSolutions LC_LineMath::convexHull(const RS_VectorSolutions& points){ RS_VectorSolutions sol = points; // ignore invalid points auto it = std::remove_if(sol.begin(), sol.end(), [](const RS_Vector& p) { return ! p.valid;}); sol = {{sol.begin(), it}}; if (sol.size() <= 1) { return sol; } // find the left-most and lowest corner std::sort(sol.begin(), sol.end(), compareCoordinates); // avoid duplicates RS_VectorSolutions hull{{sol.at(0)}}; for(size_t i = 1; i < sol.size(); ++i) { if (hull.back().distanceTo(sol.at(i)) > RS_TOLERANCE) { hull.push_back(sol.at(i)); } } if (hull.size() <= 2) { return hull; } // soft by the angle to the corner std::sort(hull.begin() + 1, hull.end(), [lowerLeft=hull.at(0)](const RS_Vector& lhs, const RS_Vector& rhs) { return lowerLeft.angleTo(lhs) < lowerLeft.angleTo(rhs); }); // keep the farthest for the same angle sol = {hull.at(0), hull.at(1)}; for (size_t i = 2; i < hull.size(); ++i) { const double angle0 = sol.at(0).angleTo(sol.back()); const double angle1 = sol.at(0).angleTo(hull.at(i)); if (RS_Math::equal(angle0, angle1, RS_TOLERANCE)) { if (sol.at(0).distanceTo(hull.at(i)) < sol.at(0).distanceTo(sol.back())) { sol.back() = hull.at(i); } } else { sol.push_back(hull.at(i)); } } // only keep left turns hull = {sol.at(0), sol.at(1)}; for (size_t i = 2; i < sol.size(); ++i) { const size_t j = hull.size() - 1; if (isCounterClockwise(hull.at(j-1), hull.at(j), sol.at(i))) { hull.push_back(sol.at(i)); } else { hull.at(j) = sol.at(i); } } return hull; } double LC_LineMath::angleFor3Points(const RS_Vector& edgePoint1, const RS_Vector& intersection, const RS_Vector& edgePoint2) { double angle1 = intersection.angleTo(edgePoint1); double angle2 = intersection.angleTo(edgePoint2); double angle = RS_Math::getAngleDifference(angle1, angle2, false); angle = RS_Math::correctAngle0ToPi(angle); // double angle = RS_Math::correctAngle(angle1-angle2); return angle; }