xy-plotter / xy

:pencil2: node.js for Makeblock XY plotter v2.0
MIT License
24 stars 6 forks source link

Support SVG <circle> #10

Open jvolker opened 6 years ago

jvolker commented 6 years ago

First of all, thanks a lot for creating this software!

I tried loading SVGs I created and exported from Affinity Designer. This is an example: image The SVG file: circle-rect.svg.zip

Unfortunately, it seems that circles are not going to be picked up. The plots and exported PNGs do not contain any circles. Rectangles show up though: current-job-svg

Loading this SVG heart icon worked as well: https://thenounproject.com/search/?q=dsf&i=350276

Are there any certain specifications or limitations to the SVGs you can load?

Thanks!

arnaudjuracek commented 6 years ago

At this point, SVGs are fully converted to <path> object using svgo : svg.js#L16-L32 Apparently, the plugins selected are not enough to convert <circle> to SVG <path>.

I will investigate on this. In the meantime, try to use <path> only SVG, or directly the built-in draw API

Also note that the firmware is currently not able to draw real curves, and instead use a subset of straight lines.

arnaudjuracek commented 6 years ago

Also, note that like xy-plotter/xy-server, a brand new version xy-plotter/xy will soonish be released.

jvolker commented 6 years ago

Thanks!

Also note that the firmware is currently not able to draw real curves, and instead use a subset of straight lines.

Good to know. Is that why circles (from the built-in API) are drawing quite slowly at the moment?

a brand new version xy-plotter/xy will soonish be released

Great news! Is the new version going to support curves as well?

arnaudjuracek commented 6 years ago

I checked how other plotters deal with curve drawing, and the majority seems to use the same straight line curve approximation as the xy's firmware.

That means that the next version will certainly use the same approach, and draw small straight lines instead of real curves. IMHO this isn't an issue, but the speed could be one. You could try setting manually the speed before calling a circle command :

...
job.setSpeed(0.8).circle(x, y, radius)
job.resetSpeed()
...

Setting manually the speed will use a linear interpolation instead of the default eased one.

jvolker commented 6 years ago

Thanks for investigating this.

I made some tests with different speeds but realized it made not much difference (see further down this post).

Different numbers of sides

I then realized there is, of course, the option of setting the number of sides from default=100. My finding is that a circle of 2mm radius (which is sort of what I'm looking after right now) looks close to perfect with around 15-30 sides. I very roughly measured the time to draw each of the circles using the default eased speed.

100 sides = 5s 30 sides = 1s

There is a lot of room for optimization it seems. I assume the number of sides should be optimized depending on the circle's size.

I quickly looked into the Axidraw repo and found this:

Convert circles and ellipses to a path with two 180 degree arcs.

Not sure, but aren't those paths curves or multiple straight lines as well?

And in the wiki of StippleGen under "Saving a Stipple Drawing" it reads:

Properly tuned for making dots rapidly, the Eggbot can plot about four stipples per second

'Stipples' are actually circles. I wonder what sort of optimization they do? Maybe even drawing dots only if the circles are really small? Checking the corresponding repos might be worth it.

Different speeds

I've tried drawing a 2mm circle with the default 100 sides using setSpeed() to 0.8, 0.95 or even 1.0 and compared it to eased speed as well. There is not much difference I would say.

Drawing a circle with 30 sides using eased speed or 0.01 was smooth and relatively fast. But setting it anywhere around 0.8 or above made for a very jagged circle.

jvolker commented 6 years ago

I have implemented this answer from Stack Overflow: How to render a circle with as few vertices as possible?

function circleOptimized(_cx, _cy, _radius, _deviation = 0.075) {

  let th = Math.acos(2 * Math.pow((1 - _deviation / _radius), 2) - 1)
  let verticesNumber = (_radius <= _deviation) ? 1 : Math.ceil(2 * Math.PI / th)

  console.log(_radius.toFixed(1) + "mm radius >>> " + verticesNumber + " vertices")

  let points = []
  for (let i = 0; i < verticesNumber; i++) {
    let angle = i / verticesNumber * 2 * Math.PI;
    let point = [
      _cx + _radius * Math.cos(angle), 
      _cy + _radius * Math.sin(angle)
    ]
    points.push(point)
  }
  if (_radius > _deviation) points.push(points[0])

  job.polygon(points)

}

Tested with a few circles:

let x = 20
let y = 70
circleOptimized(x, y, 0)

for (let r = 0.1; r < 3; r += 0.3) {
  x += r * 2 + 2 
  circleOptimized(x, y, r)
}

for (let r = 4; r < 40; r *= 1.5) {
  x += r * 1.7 + 2 
  circleOptimized(x, y, r)
}

Gives:

0.0mm radius >>> 1 vertices
0.1mm radius >>> 3 vertices
0.4mm radius >>> 6 vertices
0.7mm radius >>> 7 vertices
1.0mm radius >>> 9 vertices
1.3mm radius >>> 10 vertices
1.6mm radius >>> 11 vertices
1.9mm radius >>> 12 vertices
2.2mm radius >>> 12 vertices
2.5mm radius >>> 13 vertices
2.8mm radius >>> 14 vertices
4.0mm radius >>> 17 vertices
6.0mm radius >>> 20 vertices
9.0mm radius >>> 25 vertices
13.5mm radius >>> 30 vertices
20.3mm radius >>> 37 vertices
30.4mm radius >>> 45 vertices

At least the renderings seem to be okay. I'm not too sure about smaller circles in the physical drawing though. But this might be my pen holder, which is still not sitting super tight.

I think as an improvement depending on the radius, for bigger circles the drawing speed could go up quite a bit.

arnaudjuracek commented 6 years ago

Your circleOptimized() implementation seems really cool ! I think I will use it for v3.0.0 as a default for job.circle(cx, cy, radius, sides) when no sides argument are specified, instead of an arbitrary sides = 100 value.

Eventually, it might be a good idea to implement in the firmware a real curve drawing, but I can't find any existing solution.

I don't quite understand the approach used in the official axidraw repo you mentioned : they do convert <ellipse> to <path> with arc curves, but do not seem to use them as such : their PlanTrajectory() method use segments. They do however use a far more complex speed computation (see axidraw/axidraw.py#L1576-L1640), which could lead to better result.

I looked in Fogleman's version of axidraw, and it seems that he does draw the circle as straight lines too (axi/examples/circles.py#L5-L12).

jvolker commented 6 years ago

Your circleOptimized() implementation seems really cool ! I think I will use it for v3.0.0 as a default for job.circle(cx, cy, radius, sides) when no sides argument are specified, instead of an arbitrary sides = 100 value.

I'm glad if it helps.

Eventually, it might be a good idea to implement in the firmware a real curve drawing, but I can't find any existing solution.

In the official XY-Plotter-2.0 repo, there seems to be arc drawing in the firmware. But in the end, it's all resolved to linear movements, and it doesn't really matter if that happens in the firmware or the software, does it?

I don't quite understand the approach used in the official axidraw repo you mentioned : they do convert to with arc curves, but do not seem to use them as such : their PlanTrajectory() method use segments.

After converting to and before PlanTrajectory() there seems to be another conversion happening in here which "Turn[s] this path into a cubicsuperpath (list of beziers)". Not quite sure what that is though.

They do however use a far more complex speed computation (see axidraw/axidraw.py#L1576-L1640), which could lead to better result.

Indeed, that looks promising. Would be interesting to see the difference.

jvolker commented 6 years ago

At this point, SVGs are fully converted to object using svgo : svg.js#L16-L32 Apparently, the plugins selected are not enough to convert to SVG .

According to https://github.com/svg/svgo/pull/818 it requires svgo 1.0.0 to convert circles and ellipses to paths. This project currently uses svgo 0.7.0 Switching to the latest version 1.0.5 throws an error ⚠️ Warning: /path/to/svg-file.svg doesn't contain any valid points. and the exported PNG is empty. Looks like more work needs to be done.

Also not sure if it might be a problem that flatten transforms (groups) is not implemented in svgo yet.

Maybe https://github.com/stadline/svg-flatten could help in the meantime?

jvolker commented 6 years ago

My example from the beginning of this thread works with svg-flatten using the following code:

const plotter = require('xy-plotter')()
const svgFlatten = require('svg-flatten')
const fs = require('fs');

let flattendSvgPath = 'tmp-flattened.svg'

// read SVG into string
let svgString = fs.readFileSync('circle-rect.svg', 'utf8')
// convert all objects to paths and write to disk
fs.writeFileSync(flattendSvgPath, svgFlatten(svgString).pathify().value());

const job = plotter.Job('svg')
job.svg(flattendSvgPath, {
  x: 0,       // x coordinate
  y: 150,       // y coordinate
  width: 150,   // custom width
  height: 150,  // custom height
  // angle: 90,    // angle in degrees
  origin: [0, 1] // origin point of the transformation
})

const file = plotter.File()
file.export(job, path.join(__dirname, job.name + '.png'))

But in the rendered PNG the circles have clearly visible vertices:

image

The preview of tmp-flattened.svg in macOS doesn't have that issue:

image

I tried to use const plotter = require('xy-plotter')({ decimals : 4 }) but it didn't help. I'm wondering where this comes from?