ajstarks / svgo

Go Language Library for SVG generation
Other
2.15k stars 170 forks source link

Units Of Measure (UOM) and Precision #18

Closed swill closed 8 years ago

swill commented 9 years ago

It is not clear to me what the units are for the 'x' and 'y' coordinates. I would like to work with millimeters and have better precision than a one millimeter. Since all the functions seem to take []int instead of []float it is not possible to get better than whole number precision.

Is there any way to solve this problem? Thx...

ajstarks commented 9 years ago

See the StartUnit function: http://godoc.org/github.com/ajstarks/svgo#SVG.Startunit

swill commented 9 years ago

So if I want to have precision of less than a millimater I would have to do my units of measure in micrometers?

Since everything expects the 'x' and 'y' values to be ints, I can't use mm as a uom if I need precision down to 0.1mm.

Is doing everything in micrometers my only option?

swill commented 9 years ago

If I used mm as my units of measure and did 100x for all my values and then scaled everything down by 100x when I am finished, would I lose the precision (down to the closest int)? Or would my coordinates keep the correct precision and basically result in fractional coordinates?

swill commented 9 years ago

Is this how I am supposed to be using 'startunit'? It does not appear to do anything. There is no list of 'units' anywhere to specify what a valid input is.

Here is some test code...

package main

import (
    "github.com/ajstarks/svgo"
    "os"
)

func main() {
    f, err := os.Create("./example.svg")
    if err != nil {
        panic(err)
    }
    s := svg.New(f)
    s.Startunit(50, 50, "mm")
    s.Scale(0.5)
    s.CenterRect(25, 25, 25, 25, "fill:none;stroke:black")
    s.Gend()
    s.End()
}
ajstarks commented 9 years ago

you can use StartviewUnit to specify the viewbox:

package main

import (
    "github.com/ajstarks/svgo"
    "os"
    "fmt"
)

func box(s *svg.SVG, x, y, w, h int) {
    s.CenterRect(x, y, w, h, "fill:none;stroke:black;stroke-width:1")
}

func main() {

    f, err := os.Create("mm.svg")
    if err != nil {
        fmt.Fprintf(os.Stderr, "%v\n", err)
        os.Exit(1)
    }
    defer f.Close()
    m := svg.New(f)
    m.StartviewUnit(50, 50, "mm", 0, 0, 100, 100)
    box(m, 10,10,10,10)
    m.End()

    f, err = os.Create("cm.svg")
    if err != nil {
        fmt.Fprintf(os.Stderr, "%v\n", err)
        os.Exit(2)
    }
    c := svg.New(f)
    c.StartviewUnit(50, 50, "cm", 0, 0, 100, 100)
    box(c,10,10,10,10)
    c.End() 
}
swill commented 9 years ago

So we are definitely getting closer. The following code allows me to create an svg with objects with less than a mm of precision. I have validated this is working by converting the SVG > EPS > DXF and validated in my CAD software. So we are definitely making progress...

It is definitely a super annoying way to have to go about it. It would be MUCH nicer if all these functions would just take a float instead of an int. It makes no sense to me why we are limiting these functions to only ints. That is not according to the SVG spec because according to the spec these can all be floats.

Anyway, here is the code I have so far which is giving me 0.01mm precision.

package main

import (
    "fmt"
    "github.com/ajstarks/svgo"
    "os"
)

const (
    PRECISION = 100 // => 0.01 mm precision
)

func main() {
    f, err := os.Create("./example.svg")
    if err != nil {
        panic(err)
    }
    defer f.Close()

    width := 50
    height := 50
    scale := 1.0 / PRECISION
    stroke := PRECISION / 15.0

    s := svg.New(f)
    s.StartviewUnit(width, height, "mm", 0, 0, width, height)
    s.Scale(scale)
    s.CenterRect(
        int(25*PRECISION),
        int(25*PRECISION),
        int(13.25*PRECISION),
        int(13.25*PRECISION),
        fmt.Sprintf("fill:none;stroke:black;stroke-width:%f", stroke))
    s.Gend()
    s.End()
}
ajstarks commented 9 years ago

FWIW, I frequently use a function like this:

// vmap maps one interval to another func vmap(value float64, low1 float64, high1 float64, low2 float64, high2 float64) float64 { return low2 + (high2-low2)*(value-low1)/(high1-low1) }

to map my world coordinates to my canvas. See examples in the svgo clients.

swill commented 9 years ago

I will be completely honest with you. I didn't really follow what you are doing here. I am assuming your coord system and the canvas size are not the same size, so you are doing this to map a coord in your world to the equivalent place on the canvas. Interestingly, you are returning a float, so you must have to cast that to an int when you draw because the coords can only be ints. Does that sound about right?

I had a look around for the svgo clients you mentioned, but I don't think I found what you were referring to.

I am probably going to fork your repo and try my hand at adding support for floating point coords. I will make sure to not change the existing function declarations, but I will overload with different variables. I may fail horribly, but it is worth my trying.

So instead of just having this:

Polygon(x []int, y []int, s ...string)

We would also have (as an example):

Polygon(x []float64, y []float64, s ...string)

Not sure if I want to use float32 or float64, need to do some reading, but thats the idea...

The reason I am looking into doing this is because I have written a CAD generation software in python based on FreeCAD and there are many limitations that are really bothering me. It is slow (its python) and FreeCAD was never designed to have multiple instances running at the same time. I am getting around this by spawning LXC containers for my workloads, but this is becoming cumbersome. Ideally I can use this work as a building block and rebuild everything from scratch with this as the engine. I will be losing ing 3d renders and such, but hopefully I can better handle distributed CAD generation with this. We will see... Probably too much back story, but this is why the precision issue is such a big deal for me.

swill commented 9 years ago

Well that was remarkably easy. I have not gone through the whole lib, but I have done a couple functions and it works beautifully.

This is what my code looks like now:

package main

import (
    "fmt"
    "github.com/swill/svgo"
    "os"
)

func main() {
    f, err := os.Create("./example.svg")
    if err != nil {
        panic(err)
    }
    defer f.Close()

    s := svg.New(f)
    s.StartviewUnit(50, 50, "mm", 0, 0, 50, 50)
    s.CenterRectF(25, 25, 13.25, 13.25, fmt.Sprintf("fill:none;stroke:black;stroke-width:0.2"))
    s.End()
}

Unfortunately Go does not support function overloading (same name with different parameter types), so I had to use new function name. So far I have added:

func (svg *SVG) RectF(x float64, y float64, w float64, h float64, s ...string)
func (svg *SVG) CenterRectF(x float64, y float64, w float64, h float64, s ...string)
func dimF(x float64, y float64, w float64, h float64) string

I am not sure what naming convention I will want to use yet. I think I will probably just add a suffix to each of the existing functions which only supports ints. The suffixes I am considering are "F", "Float", ...

Thoughts on this? Will you even consider a pull request with this functionality or should would I be doing this just for my own usage?

ajstarks commented 9 years ago

Floating point support has been a long-standing issue, thanks for the change. It will be useful for your usage to soak a bit before committing the change.

ajstarks commented 9 years ago

Also, what precision are you using for the floats?

swill commented 9 years ago

What I will do is add support for floating point numbers in the whole lib and use it for my own use for now. I will keep my fork active and fix any issues with what I am doing as I build my tool.

Once I am comfortable with my code and have it in a good state, I will do a pull request and we can review the changes together if you would like.

As for precision. Right now I am just using "%f" because I was just doing a bit of testing. I think I would probably default to "%.3f", but I would like to make that configurable. Not entirely sure how I am going to do that yet. I was considering adding a function that you would call after the constructor and before the Start function that would allow you to define something like floatDecimalPlaces(f int). This would be used to set the precision for that SVG instance if you didn't want to use the default. I will have to think about it a bit, but thats my initial thought process.

I am glad you are open to my adding this support. I will slowly add floating point support in my fork and we will go from there. If you have suggestions or you want me to do anything in a specific way, just let me know. :)

swill commented 9 years ago

The more I work with Go the more I love it. :)

I am not entirely sure how I am going to set the precision yet, but this example shows how I can output it in whatever global precision we want based on a variable.

precision := 2
fmt.Printf("%.*f", precision, 78.921111111)

Which gives:

78.92
swill commented 9 years ago

Before I get too far into this implementation I figured I would give you an update.

Here is what I have built so far in addition to supporting all of the 'Start' functions.

This code uses my functions which support floats:

out := svg.New(terminal)
out.FloatDecimals = 0                                // the number of decimals our floats will have
out.StartviewUnitF(1024, 768, "mm", 0, 0, 1024, 768) // setup the view
// squares in top left
out.CenterRectF(15, 15, 20, 20, style)
out.RoundrectF(7, 7, 16, 16, 2, 2, style)
out.SquareF(9, 9, 12, style)
out.FloatDecimals = 2
out.PolygonF([]float64{10, 20, 15, 10}, []float64{20, 20, 10, 20}, style) // triangle
out.CircleF(15, 15, 1.5, style)
out.EllipseF(15, 18.25, 3.5, 1, style)
// translated ellipses right of squares
out.TranslateF(30, 12.5)       // translate to the right of the boxes
out.TranslateRotateF(5, 0, 45) // translate then rotate
out.EllipseF(0, 0, 2, 10, fmt.Sprintf("fill:red;fill-opacity:0.5;stroke:black;stroke-width:0.2"))
out.Gend()
out.RotateTranslateF(5, 0, 45) // rotate then translate
out.EllipseF(0, 0, 2, 10, fmt.Sprintf("fill:blue;fill-opacity:0.5;stroke:black;stroke-width:0.2"))
out.Gend()
out.Gend()
// rect below squares and lines
out.RectF(5, 27.5, 20, 5, style)
out.LineF(25, 26.25, 35, 26.25, fmt.Sprintf("stroke:black;stroke-width:0.2"))
out.Def()
out.MarkerF("dot", 5, 5, 8, 8)
out.CircleF(5, 5, 3, "fill:black")
out.MarkerEnd()
out.DefEnd()
out.PolylineF(
    []float64{27.5, 37.5, 37.5},
    []float64{30, 30, 22.5},
    `fill="none"`,
    `stroke="black"`,
    `stroke-width="0.2"`,
    `marker-end="url(#dot)"`)
out.End()

This produces the following file:

<?xml version="1.0"?>
<!-- Generated by SVGo -->
<svg width="1024mm" height="768mm"
     viewBox="0 0 1024 768"
     xmlns="http://www.w3.org/2000/svg" 
     xmlns:xlink="http://www.w3.org/1999/xlink">
<rect x="5" y="5" width="20" height="20" style="fill:none;stroke:black;stroke-width:0.2"/>
<rect x="7" y="7" width="16" height="16" rx="2" ry="2" style="fill:none;stroke:black;stroke-width:0.2"/>
<rect x="9" y="9" width="12" height="12" style="fill:none;stroke:black;stroke-width:0.2"/>
<polygon points="10.00,20.00 20.00,20.00 15.00,10.00 10.00,20.00" style="fill:none;stroke:black;stroke-width:0.2"/>
<circle cx="15.00" cy="15.00" r="1.50" style="fill:none;stroke:black;stroke-width:0.2"/>
<ellipse cx="15.00" cy="18.25" rx="3.50" ry="1.00" style="fill:none;stroke:black;stroke-width:0.2"/>
<g transform="translate(30.00,12.50)">
<g transform="translate(5.00,0.00) rotate(45)">
<ellipse cx="0.00" cy="0.00" rx="2.00" ry="10.00" style="fill:red;fill-opacity:0.5;stroke:black;stroke-width:0.2"/>
</g>
<g transform="rotate(45) translate(5.00,0.00)">
<ellipse cx="0.00" cy="0.00" rx="2.00" ry="10.00" style="fill:blue;fill-opacity:0.5;stroke:black;stroke-width:0.2"/>
</g>
</g>
<rect x="5.00" y="27.50" width="20.00" height="5.00" style="fill:none;stroke:black;stroke-width:0.2"/>
<line x1="25.00" y1="26.25" x2="35.00" y2="26.25" style="stroke:black;stroke-width:0.2"/>
<defs>
<marker id="dot" refX="5.00" refY="5.00" markerWidth="8.00" markerHeight="8.00" >
<circle cx="5.00" cy="5.00" r="3.00" style="fill:black"/>
</marker>
</defs>
<polyline points="27.50,30.00 37.50,30.00 37.50,22.50" fill="none" stroke="black" stroke-width="0.2" marker-end="url(#dot)" />
</svg>

Not that you can change the floating point precision at any point while you are drawing the SVG.

I have pushed my code to the float branch in my fork so you can check it out: https://github.com/swill/svgo/tree/float

If you have comments or suggestions, please let me know. I will continue on the same path I have taken so far unless otherwise directed.

swill commented 9 years ago

Unless you have any input, I am going to continue along the same path for the rest of the lib. Yes? Does that sound alright to you?

ajstarks commented 9 years ago

fine. Feel free to update with any changes

swill commented 9 years ago

I have been busy so I have not had a chance to work on this much till now.

I noticed that you are using %g for existing floating point numbers when writing to the SVG. The %g option has the following behavior.

%g %e for large exponents, %f otherwise

and %e has the following format.

%e scientific notation, e.g. -1234.456e+78

looking at this: http://www.w3.org/TR/SVG/types.html#DataTypeNumber

It does not look like SVG supports the default %e format. Based on the regular expression number ::= integer ([Ee] integer)? | [+-]? [0-9]* "." [0-9]+ ([Ee] integer)? it seems like it would not match this format.

Have you tested the %e format to validate it works? If need be I can update the code to use the new svg.FloatDecimals declaration of precision for those values as well.

I will skip this for now and come back to it later on...

stanim commented 9 years ago

@swill I've completed a floating point port of SVGo, with different precisions. Please see: https://github.com/ajstarks/svgo/issues/9. I prefer to keep the implementations for ints and floats separate, as probably they are never used together. The port is done for float64, but float32 is also possible, if there is any demand.

swill commented 9 years ago

@stanim I will check yours when I am in front of a computer. Did you just merge my code? I have developed a tool on my implementation and it is working well. I was going to do a pull request soon to see if my implementation was wanted upstream.

I will review what you have done later tonight.

stanim commented 9 years ago

@swill I did not for your code. I took a different approach by writing an automatic convertor by parsing the syntax tree. Please read svgo issue 9, where I give more details, to get the idea.

Here is my work: https://github.com/stanim/svgo

swill commented 9 years ago

@ajstarks I figure I should give you an update on this. I have been using my floating point implementation in production for a little while now and it is working well. You can see it in action at: http://builder.swillkb.com

My tool produces SVG files which can then be used to cut custom computer keyboard plates. Here is an example: https://objects-east.cloud.ca/v1/729bfb2c76f7488798aeff5bacd426f8/swillkb/74fe4c6c9898276a4af3813ebaa40d88b4bcfa43/switch_74fe4c6c9898276a4af3813ebaa40d88b4bcfa43.svg

I have ported the majority of the functions. I think there is like 5 that I have not gotten around to porting yet because I have not had a need for them and I didn't have a chance to write tests for them.

I have included the float_examples directory with examples of everything that I have ported. You can build and run it to see the output on screen. You can also produce an SVG file (for easy viewing in a browser) using the -output flag. I have included the float_examples.svg in that folder so you can verify what the output looks like.

I rebased my repo based on your current master tonight. I will send you a pull request so you can review my code and let me know if you have feedback. If you see anything you want changed, let me know and I will review it...

Thanks again for the hard work on this lib. It is working very well... :)

ajstarks commented 9 years ago

As I commented on the other thread, I'm leaning toward the gofloat method, with specified precision. Allow me some time to test.

swill commented 9 years ago

@ajstarks: Cool. Just let us know. Would you be considering adding the 'gofloat' lib as a sublib to your package so we could regenerate and have the floating point libs local to this repo? That would be ideal because then as things change in this package, we can just regenerate the floating point implementations and always know that we are working with the latest float code. Does that sense?

stanim commented 9 years ago

@ajstarks Thanks for your reaction.

@swill Thanks for your contribution and constructive attitude in this discussion. Thanks to your proposal there will be precision change ;-)

These are five possible scenarios for repository layout out of many:

[A]

github.com/ajstarks/svgo
github.com/ajstarks/svgof (float64)
github.com/ajstarks/svgof32 (float32, interesting for ARM devices)
github.com/stanim/gofloat

[B] (only if you prefer I host the floating point repositories)

github.com/ajstarks/svgo
github.com/stanim/svgof (float64)
github.com/stanim/svgof32 (float32, interesting for ARM devices)
github.com/stanim/gofloat

[C] (if in same repository less work to push new version)

github.com/ajstarks/svgo
github.com/ajstarks/svgof/float64 (package name is still "svg")
github.com/ajstarks/svgof/float32
github.com/stanim/gofloat

[D]

github.com/ajstarks/svgo
github.com/stanim/svgof/float64
github.com/stanim/svgof/float32
github.com/stanim/gofloat

[E] (feels strange, but possible, no extra work to push)

github.com/ajstarks/svgo
github.com/ajstarks/svgo/float64 (without subfolders)
github.com/ajstarks/svgo/float32 (without subfolders)
github.com/stanim/gofloat

We could also transition from [B] to [A], once you feel good about it.

I prefer to keep the gofloat as my repository as I have to maintain it. Moreover I might use it for other projects as well. gofloat does not contain any SVGo specific code, all configuration for SVGo (or another package) is done with a json file. When ready, I can do a PR to add svgo.json to the svgo repository. svgof and svgof32 can be generated with:

gofloat svgo.json

So svgo, svgof and svgof32 can be perfectly in sync and released a the same time.

Unfortunately I leave on holiday and have some strong deadlines for other projects in august. I'll try to do my best. As I need to do this in my free time, for sure in september everything should be ready, but I guess it can be much earlier. For someone who can't wait, right now the temporary solution at github.com/stanim/svgo is working already (although without precision change). If people would use this code, they just have to change the import path to eg github.com/ajstarks/svgof and that's it, as the svg package name and api is the same.

If you encounter any bug with my ports, please file an issue at github.com/stanim/svgo and I will fix it.

swill commented 9 years ago

@stanim: thanks for the hard work. Gotta stay constructive with open source software so everyone wins.

I am more of a bystander at this point, but I would vote for A. It feels the most natural as a consumer of the svgo package. Trying to figure out what fork of these types of packages to use is hard enough, its better to keep things together as much as possible.

Once the code makes it to its final(ish) location, I will start porting my production application so I can deprecate my svgo fork.

ajstarks commented 9 years ago

I'd prefer to host and will maintain both int and float versions as "official". Of course people can use gofloat to maintain their own forks as they see fit. I'll start the process when @stanim feels gofloat is ready.

At this point I'd prefer only doing a float64 version as this is the typical type for Go programs.

stanim commented 9 years ago

I moved my temporary repo to https://github.com/stanim/svgof. This allows me better to do pull requests to svgo.

swill commented 9 years ago

Has there been any movement on this? I am still using my version in production (which I should probably rebase with the current code) and it is working well. I am happy to adopt whichever approach is the final 'correct' way to handle this once it has been established...

ajstarks commented 8 years ago

The first commit of the floating point support is now up. Use the import:

import "/github.com/ajstarks/svgo/float"

The API is identical except float64 is used for lengths and dimensions, and you can specify the number of decimal digitals in the output (SVG.Decimals).

ajstarks commented 8 years ago

fixed with github.com/ajstarks/svgo/float package