In direct and faster algorithm for the length of a polyline(-segment)

Discussion forum for C++ and script developers who are using the QCAD development platform or who are looking to contribute to QCAD (translations, documentation, etc).

Moderator: andrew

Forum rules

Always indicate your operating system and QCAD version.

Attach drawing files, scripts and screenshots.

Post one question per topic.

Post Reply
CVH
Premier Member
Posts: 5082
Joined: Wed Sep 27, 2017 4:17 pm

In direct and faster algorithm for the length of a polyline(-segment)

Post by CVH » Fri Jan 16, 2026 12:12 pm

Andrew,

A Polyline is commonly seen as a chain of line-segments and/or arc-segments.
:arrow: Not entirely true :!: ... A Polyline is a chain of segments that are straight or circularly curved. e_geek

The difference is subtle, firstly, Arc shapes do not have end points like two ending vertices, Arc shapes have ending angles.
Arc shapes are defined by: center, radius, start- and end-angle and reversed or not.
None of these properties is readily available for a curved Polyline segment defined by two points and a factor.


The default method for the length of a polyline explodes the polyline to all its individual segments.
Creating all new line and/or arc shapes (RLine/RArc) and then solely to sum their length ...
... Without any further use for these shapes afterwards :!:

Hundreds to many thousands or even millions shapes are internally constructed and discarded this way. :roll:


-> For straight segments or newly created RLine shapes the length is measured in 3D:
  • RLine.getLength() exploits startPoint.getDistanceTo(endPoint) equal to sqrt(dx²+dy²+dz²)
    Supporting 3D data is debatable when not all segments are straight.
-> For not straight segments or newly created RArc shapes the 2D length is measured as the absolute sweep angle in rads times the radius.

Important here :!: : The vertex data or endpoints for such an Arc shape can/may NOT be defined in 3D with the result shown below.
For QCAD, an Arc shape can only exist in a flat plane.
Any point on the curve, including start and end is at the same level in Z as the center position.
The same is true for a Circle shape and the related Ellipse shape.

The method is not guarded for this and the summed total length is faulty in every perspective.
Without additional data to slant the plane in 3D, an Arc in top view from 2 points in 3D is part of an Helix where the pitch (dz) can be zero or not.
Similar to a G-code motion G2 or G3 where Z is not equal to the previous position in XYZ.

Faulty3Dpoly.png
White is one polyline as rendered by QCAD, dotted green is the intended bulging segment (90°)
Faulty3Dpoly.png (4.76 KiB) Viewed 1311 times


:arrow: For bulging segments it is considered !extremely obsolete! to define:
  • CW/CWW nature, chord angle, radius minus sagitta, bisector angle, center, start-angle and end-angle for a new RArc shape.
There the length of a circular curve limited to 2D is as simple as sweep times radius where:
  • The sweep angle is well defined given the fact that the bulging factor is equal to: tan(sweep/4) => sweep = atan(bulge)*4
  • The radius can be directly extracted from vertex data in various ways.
    In QCAD the conversion from bulging to Arc is based on a sine and therefor we can opt for: radius = chord/2/sin(sweep/2)
Combined this leads to:
  • Bulge factor is zero: segment length = chord or the distance between the two vertices (2D < ?! > 3D ...).
  • When not zero: segment length = chord/sin(halfSweep)*halfSweep
    Where the chord must :!: be in 2D and halfSweep = atan(bulge)*2.

In any case, 'segment length = chord/sin(halfSweep)*halfSweep' is a much simpler expression than the length of the shape returned by:

Code: Select all

        double radius;
        RVector center;
        RVector middle;
        double dist;
        double angle;

        middle = (p1+p2)/2.0;
        dist = p1.getDistanceTo(p2)/2.0;
        angle = p1.getAngleTo(p2);

        // alpha can't be 0.0 at this point
        radius = fabs(dist / sin(alpha/2.0));

        double rootTerm = fabs(radius*radius - dist*dist);
        double h = sqrt(rootTerm);

        if (bulge>0.0) {
            angle+=M_PI/2.0;
        } else {
            angle-=M_PI/2.0;
        }

        if (fabs(alpha)>M_PI) {
            h*=-1.0;
        }

        center.setPolar(h, angle);
        center+=middle;

        double a1;
        double a2;

        a1 = center.getAngleTo(p1);
        a2 = center.getAngleTo(p2);

        return QSharedPointer<RShape>(new RArc(center, radius, a1, a2, reversed));
Simpler is also less prone to accumulated errors in floating point and thus more accurate.
Not that the overall length of various test objects differ for more than one to rarely a few last digits. :)


Some considerations have to be made:
  • QCAD restricts the sweep angle of a bulging segment to not more than 2Pi-RS.PointTolerance (See RPolyline::getSegmentAt(i))
    Reverting to a line segment instead of nearly a full circular segment
    Associated limit for the bulge factor in absolute is then 4.0e9 or more.
  • QCAD also reverts to a line segment on a bulge factor less than 1.0e-6 in absolute.
    There the sagitta is bulge*chord/2, the sagitta would have the minimum processable length of 1e-6 when the chord is over 2 units long.
    In such a case the radius would be a fraction over 500000 units, practically considered to be straight thus.
    The 'minimum processable length' is derived from: getAngle(), getVectorTo(), ...
  • QCAD does not include Null segments in an explosion and doesn't sum a NaN length.
    This only happens when the segment index would be out of range (getSegmentAt(i)).
  • Newly created RLine or RArc shapes are not validated, but it is verified whether the length of those shapes has a normal value.
  • The order of expressions in chord/sin(halfSweep)*halfSweep matters.
    Opted for the order with the more and better matching results for a larger sample size of polylines.

All combined in the JS code below:

Code: Select all

// Given is the Polyline 'poly'

    var vertices, bulges;           // Arrays
    var i, iMax,                    // Integers
    var chord, bulge, halfSweep;    // Doubles
    var length;                     // Result: Double

    // Initial values:
    vertices = poly.getVertices();
    bulges = poly.getBulges();
    iMax = vertices.length - 1;    // Define once, use on every iteration
    length = 0.0;                  // Initial value

    // Sum all the lengths of segments between vertices:
    for (i=0;i<iMax;i++) {
        chord = vertices[i].getDistanceTo2D(vertices[i+1]);
        bulge = bulges[i];

        // Avoid abnormal value NaN and Infinite:
        // Comparing with NaN is always false
        if (!RMath.isNormal(chord)) {
            continue;
        }

        // Diversify on bulge factor:
        // Avoid an abnormal value, exclude beyond limits
        // Also avoiding a division by zero
        if (!RMath.isNormal(bulge) || Math.abs(bulge) < 1.0e-6 || Math.abs(bulge) >= 4.0e9) {
            length += chord;
        }
        else {
            halfSweep = 2 * Math.atan(bulge);
            length += chord / Math.sin(halfSweep) * halfSweep;
        }
    }

    // Add logically closing segment length:
    // Avoiding a construction like 'vertices[i%vertices.length]' ...
    // ... The remainder of a division of integers(+) on every iteration
    if (poly.isClosed()) {
        chord = vertices[i].getDistanceTo2D(vertices[0]);
        bulge = bulges[i];

        // Avoid abnormal values NaN and Infinite:
        if (RMath.isNormal(chord)) {
            // Diversify on bulge factor:
            if (!RMath.isNormal(bulge) || Math.abs(bulge) < 1.0e-6 || Math.abs(bulge) >= 4.0e9) {
                length += chord;
            }
            else {
                halfSweep = 2 * Math.atan(bulge);
                length += chord / Math.sin(halfSweep) * halfSweep;
            }
        }
    }


The length of a segment at given index is about half of this code.
The method can also be chopped up in 2 parts: getLength calling getSegmentLengthAt(i) for each index.


Regards,
CVH

Post Reply

Return to “QCAD Programming, Script Programming and Contributing”