evil-mad / axidraw

Software for the AxiDraw drawing machine
GNU General Public License v2.0
434 stars 131 forks source link

"Thinking" Pause before drawing long curves #38

Closed simlu closed 7 years ago

simlu commented 7 years ago

This is a problem because the pen does not raise from what it was drawing before until this "thinking" pause is over and causing a lot of ink to spill onto the page.

I've worked around this by splitting my curves up, but preferably the pen would lift before.

oskay commented 7 years ago

Please state which version of the software you are using and describe the issue in detail. Please say how to reproduce the issue, with an example if needed.

Describing why it is a problem without saying what the problem is does not help lead to a solution.

simlu commented 7 years ago

Sure thing. Software I'm using is

When a spline is drawn, the extensions seems to send all the data for the spline before the plotter starts the actual work. This works fine when drawing short splines. However sending can take a long time when a long spline is drawn (several seconds). In this case the pen sits on the paper until all the data is received.

I've uploaded a sample file here.

Here is a picture of the linked file plotted. The problem is clearly visible in the middle of the picture.

GitHub Logo

I'm suspecting a trade off here since - if the next spline continuous exactly where the last one finished - the pen is never raised and the plotter simply continuous. So maybe splitting up the curve is the best solution?

PS: Also closing the issue right after you comment on it isn't very respectful to the reporter. I think the general consensus is to wait for further clarification for at least a day (even though I appreciate that you are trying to keep the issue tracker clean).

oskay commented 7 years ago

No data is actually sent to the plotter before beginning a given path; the delay is not in sending data, but in computing the path on your computer prior to sending any data.

On the example picture that you show, with the bleed in the middle there, that looks to me like a very short path element-- and you're seeing a pause of several seconds there? If this is the case, then there is something deeply wrong here.

The case of bleed due to a pen-down delay between two complex paths with zero gap between them is a very interesting corner case that I had not considered. The other case, where we can skip an extra pen lift and lower if the gap is small enough, is extremely common. You can alter this behavior by changing the value of the MinGap parameter in axidraw_conf.py. Using a negative value (e.g., -1) should fully disable that, causing it to raise the pen at the end of every pen-down subpath.

I apologize if I seemed harsh there; it was not intended. Temporarily closing the issue seemed less extreme than marking it as invalid.

simlu commented 7 years ago

Ah, that makes perfect sense now. Is the reason that this is computed "on-the-fly" a memory concern on the sending computer? If that is the case, potentially the path could be split up further for the computation?

That is correct, this happens at the end of a very short line. However the next spline is extremely long. So while the computer is preparing that, the pen just sits there at the end of the short spline.

I don't think the MinGap parameter will solve the problem mentioned here. Or does setting it to -1 force a pen lift before computing the next path?

I do really like the solution of splitting up the spline into multiple splines based on your reply here. I'm using no more than 3000 spline segments currently and that keeps the delay down to a few hundred ms.

Overall this is an interesting edge case, but not a priority issue to resolve from my side.

oskay commented 7 years ago

The control board on the AxiDraw is essentially used as a real-time controller, without storing any of the onboard path (save for the next movement to be executed). This is a reasonable tradeoff, in that it requires the path planning to be done on the computer, but (1) the computer has much more computing capability than an (inexpensive) embedded device and (2) there is no practical limit on path complexity or length that would otherwise exist due to memory limitations on the control board.

Setting MinGap to -1 does not force a pen lift, but it disables the routine that checks to see if we can skip a zero-length pen-up movement.

My first impression is that the delay that you are seeing occurs when first parsing a given path element. There are a couple of initial operations that are done on the full path before checking for even/odd path elements (which translate to pen down/pen up movements).

If possible, I would recommend separating your path into separate objects (path elements), each of which describes a set of connected pen-down movements. Alternately, as you suggest, using shorter splines might help with the situation.

Otherwise, we might be able to add a new (off by default) feature that could force a pen-up movement if (for example) the path that we are parsing is above a certain length.

simlu commented 7 years ago

So MinGap to -1 might fix it if the skip check is the problem (unlikely imo).

Otherwise, we might be able to add a new (off by default) feature that could force a pen-up movement if (for example) the path that we are parsing is above a certain length. - This sound like a solution here. However I'm not sure if determining the path length is already a bottleneck?

Can we use the warning output to log time spend in the various steps or is there an easier way to debug this?

oskay commented 7 years ago

Logging does not presently monitor time spent in these routines, but you could potentially add it (in your own copy) if you like. If you search for time.time() - self.start_time, that (and the area around it) give some snippets that could be repurposed for timing a given activity and reporting it.

It would be helpful if you could provide an example file (perhaps stripped to the relevant paths) in order to begin to approach this and evaluate the severity of the issue.

Also, which platform and OS version are you using?

simlu commented 7 years ago

Sounds good! I'll see how I find time to take a look at it.

I've already posted an example file in the first response. The issue should happen at the very beginning of the file (could potentially be stripped down a little more).

I'm currently using Windows 7 for the plotting.

oskay commented 7 years ago

Yes, this file is a little unusual. (Even my text editor complained about the size of the lines....)

I'll take a look.

oskay commented 7 years ago

I can reproduce the issue here. It looks like the file has relatively few objects, but one of them is huge. If you ungroup the objects and put that one lowest, it should remove the immediate issue (for this file).

oskay commented 7 years ago

Here is a software fix that you can try, which adds a pen-lift move while computing the move (implementing the idea to "force a pen-up movement if (for example) the path that we are parsing is above a certain length.")

axidraw.py.zip

oskay commented 7 years ago

Have you been able to test this?

simlu commented 7 years ago

I solved this for now by splitting long lines when generating the file. So this is not required for me at this point.

The fix seems to work fine though! What is d in this case? Should we lower the check to maybe 3000? This seems to be the sweet spot on my relatively powerful machine (4ghz).

Diff is

if (len(d) > 10000):         # Raise pen when computing extremely long paths.
  if (self.penUp != True):    # skip if pen is already up
    self.penRaise()
oskay commented 7 years ago

That sounds like progress. The issue was as I had guessed, that the path data length (d) was very long and took a moment to parse. We could certainly set it to a shorter threshold.

simlu commented 7 years ago

It does! For a simple Bezier Curve, would d be 2 or 4? Or something else? Just trying to figure out what threshold matches my threshold here.

oskay commented 7 years ago

In this case, we're checking the length in characters of the path data element from the SVG file. Typically, that contains a list of ordered pairs in the units that you've chosen, with some modifiers to indicate the type. You can look up the specification of SVG for more information: https://www.w3.org/TR/SVG/paths.html#PathData

simlu commented 7 years ago

Ah, so this is not actually the path length, but the path representation or data length as you correctly wrote! I missed that when reading your reply!

I think we should then instead consider the amount of spaces contained as this would tell use the point count in path.

So something like

if (d.count(' ') + 1 > 3000):     # Raise pen when computing extremely long paths.
  if (self.penUp != True):        # skip if pen is already up
    self.penRaise()

What do you think?

oskay commented 7 years ago

I'm going to stick with the simple length heuristic for the moment. No it is not precise in any sense, but it is dramatically faster.

Counting the spaces is certainly a more accurate measurement, but the basic question of whether we should raise the pen (to avoid a pen-down delay because we're working with a very long string) should be asked and answered as quickly as possible.

Starting with the long string from that file (d is 1465320 characters long, with 151999 spaces), I tried these two tests:

start_time = time.time()    
for i in range(100):    
    q = d.count(' ')
elapsed_time = time.time() - start_time

elapsed_time: ~ 0.18 s (or about 2 ms per pass)

start_time = time.time()    
for i in range(100):    
    q = len(d)
elapsed_time = time.time() - start_time

elapsed_time: ~6.2e-05 s (pretty damned fast)

I'll take your advice about setting the threshold to 3000, and add this feature to the codebase.

simlu commented 7 years ago

Didn't think it would be that much slower. The 3k was for points. Not for characters. I think for characters 10k or even more should be fine.

oskay commented 7 years ago

I'm certainly open to other types of tests as well (so long as they are quick). I see on the point length... but it may be better to err on the side of caution here; some people do not have fast computers.

simlu commented 7 years ago

Honestly I'll like to see how the count version performs on an "ordinary" file. I have a feeling there won't be a big difference and counting the points should be more accurate than using the string length. 2ms for that many points isn't bad if it's proportional to number of characters.

oskay commented 7 years ago

It's a tricky balance. On the one hand, having the pause there can cause some pen-down "bleed". On the other hand, raising and lowering the pen takes much longer. On the third hand, computing exactly how long the string is (in terms of movement points) adds additional time.

There is no direct correspondence between string length and path complexity (especially since many different levels of precision may be used), but the basic question of "is this an extraordinarily long string" is fast to answer and is (in my mind at least) sufficient to swing the balance towards lifting the pen there. If you think that extra precision is merited, or that the threshold should be elsewhere, you might want to run some tests to see where you feel the tradeoff is better.

There are also some other possible approaches, such as using a different method to calculate the number of points, or perhaps sampling a smaller section of the path and using that to form an estimate.

Here is one dramatically faster estimate:

q = len(d)
if (q > 3000):
    sample = d[:100]            # get first 100 chars of d
    spacesInSample = sample.count(' ')  # count spaces in first 100 chars

Then, q * spacesInSample / 100 is a good estimate of the total number of spaces in the string d.

Testing this on the string in this example, I found

Estimated total spaces: 146532
The total number of spaces is actually: 151999

We can then use:

if ((spacesInSample * q/100) > 3000):     # Raise pen when computing extremely long paths.
  if (self.penUp != True):                # skip if pen is already up
    self.penRaise()

or simplify this to if ((spacesInSample * q) > 300000)

usmanm commented 4 years ago

Hi @oskay! I'm seeing the same issue. I've attached the input SVG, the output SVG (via --preview), and a photo of output with the two points where the plotter stayed down long enough for the ink to bleed. I used the Inkscape extension to print this: Inscape 0.92, axidraw 2.5.6.

axidraw.zip

What's the best way to go about debugging this? Any help is super appreciated!

oskay commented 4 years ago

@usmanm What is it that makes you think this is a related issue?