meerk40t / svgelements

SVG Parsing for Elements, Paths, and other SVG Objects.
MIT License
135 stars 29 forks source link

.point(fraction) is not linear on curves #188

Closed SimonbJohnson closed 2 years ago

SimonbJohnson commented 2 years ago

I'm using the .point() function to return points along a curve, but these are not linear as expected. Below is sample code and sample SVG that replicates the problem. The SVG contains a straight line represented by a curve created by Inkscape. In the table below we can see the change in x starts as ~49 and by the end the change is ~0.25 for the same fraction move along the line. If the same function is parsed along a line the results are as expected.

Percent, x, y

0.043478260869565216 111.11512694673719 161.7190265408256
0.08695652173913043 160.45710288472165 161.7190265408256
0.13043478260869565 205.31667870144724 161.7190265408256
0.17391304347826086 245.9073020217358 161.7190265408256
0.21739130434782608 282.44242047040933 161.7190265408256
0.2608695652173913 315.13548167228936 161.71902654082558
0.30434782608695654 344.19993325219804 161.7190265408256
0.34782608695652173 369.849222834957 161.7190265408256
0.3913043478260869 392.29679804538836 161.7190265408256
0.43478260869565216 411.7561065083138 161.71902654082564
0.4782608695652174 428.44059584855506 161.7190265408256
0.5217391304347826 442.56371369093415 161.7190265408256
0.5652173913043478 454.33890766027287 161.7190265408256
0.6086956521739131 463.9796253813932 161.7190265408256
0.6521739130434783 471.69931447911677 161.7190265408256
0.6956521739130435 477.7114225782655 161.7190265408256
0.7391304347826086 482.22939730366136 161.7190265408256
0.7826086956521738 485.46668628012617 161.7190265408256
0.8260869565217391 487.6367371324816 161.7190265408256
0.8695652173913043 488.95299748554976 161.7190265408256
0.9130434782608695 489.62891496415216 161.7190265408256
0.9565217391304348 489.87793719311105 161.7190265408256

Code:

from svgelements import SVG, Shape, Move, Path, Line, Arc, CubicBezier, QuadraticBezier, Close, Point

svg = SVG.parse('accuracy_test_drawing2.svg', reify=True)

for element in svg.elements():
    #convert all shapes to paths
    print('raw')
    print(element)
    if isinstance(element, Shape):
        if not isinstance(element, Path):
            element = Path(element)
        print('process')
        print(element)
        for part in element:
            smoothing = 0.05
            print(part)
            if not isinstance(part, Move) and not isinstance(part, Close):
                print('new part')
                samples=round(part.length(error=1e-5) * smoothing)+1
                for pos in range(1,(samples)):
                    fractionPos = 1/samples*pos
                    point = part.point(fractionPos)
                    print(fractionPos,point[0],point[1])

SVG:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->

<svg
   width="210mm"
   height="297mm"
   viewBox="0 0 210 297"
   version="1.1"
   id="svg7484"
   inkscape:version="1.2.1 (9c6d41e, 2022-07-14)"
   sodipodi:docname="accuracy_test_drawing.svg"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:svg="http://www.w3.org/2000/svg">
  <sodipodi:namedview
     id="namedview7486"
     pagecolor="#ffffff"
     bordercolor="#000000"
     borderopacity="0.25"
     inkscape:showpageshadow="2"
     inkscape:pageopacity="0.0"
     inkscape:pagecheckerboard="0"
     inkscape:deskcolor="#d1d1d1"
     inkscape:document-units="mm"
     showgrid="false"
     inkscape:zoom="0.2102413"
     inkscape:cx="-359.11117"
     inkscape:cy="573.15094"
     inkscape:window-width="1440"
     inkscape:window-height="785"
     inkscape:window-x="0"
     inkscape:window-y="25"
     inkscape:window-maximized="0"
     inkscape:current-layer="layer1" />
  <defs
     id="defs7481" />
  <g
     inkscape:label="Layer 1"
     inkscape:groupmode="layer"
     id="layer1">
    <path
       style="fill:none;stroke:#000000;stroke-width:0.964999;stroke-opacity:1"
       d="m 15.101695,42.788136 c 114.521185,0 114.521185,0 114.521185,0"
       id="path7542" />
  </g>
</svg>
tatarize commented 2 years ago

This is accurate (though not for circular arcs) bezier curves operate like that. Different slices of T even linear slices of T are not linear. In fact, the speed of the curve changes. The value submitted by .point() is t as that's how the curves are converted. I don't know how you could properly calculate purely evenly spaced segments except with a recursive brute force algorithm.

You could possibly use bresenham-zingl algorithms to draw the curves with for a suitably small distance and raster the curve into discrete points and sample it each n-pixels or so. But, this mostly is to do with the nature of Bezier curves. You can find the position of t but not necessarily the curve that is exactly d distance long for each subdivision.

tatarize commented 2 years ago

https://pomax.github.io/bezierinfo/#tracing

If something along that line is required. Should be pretty solid explanation of the problem, and a summary of the various solutions.

SimonbJohnson commented 2 years ago

Hi, this makes complete sense! I will use the above linked technique to search for linear increments to a certain tolerance. Thank you. Happy to share my implementation here when done for others. Would this be something you would be interested in being implemented in the library?

tatarize commented 2 years ago

Perhaps if the code was very short and worked in numpy too. It's a bit outside the regular bounds of the project. And it would seem like the same would be needed for non-circular arcs to provide such functionality across the board. It's not out of the question but the answer is probably no. But, if it's like three really pretty lines I could see an implementation being reasonable.

SimonbJohnson commented 2 years ago

I'm unlikely to make something that concise, so i will close for now and share a code snippet here when I implement for reference. Thanks