Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Write to svg #102

Closed
ghost opened this issue Apr 1, 2021 · 14 comments
Closed

Write to svg #102

ghost opened this issue Apr 1, 2021 · 14 comments

Comments

@ghost
Copy link

ghost commented Apr 1, 2021

is it also possible to write elements to a svg file?

@tatarize
Copy link
Member

tatarize commented Apr 1, 2021

Not strictly with the library as is, you could certainly use something like svgwrite for that but, this module is mostly concerned accurately rendering the geometry. You usually end up with a bunch of paths and you can write that stuff out pretty easily since they all have d() as a command to produce the path_d strings. But, it's not strictly concerned with element writing.

@ghost
Copy link
Author

ghost commented Apr 1, 2021

Thanks for your reply! Is it possible to apply transformations to objects at object init-time? Like applying rotations to a Line object, so that calling the bbox method returns the correct box of the rotated object?

@tatarize
Copy link
Member

tatarize commented Apr 2, 2021

Yeah. If you have a SimpleLine you can multiply it by a Matrix or another transformation.

>> from svgelements import SimpleLine
>> (SimpleLine((0,0), (100,100)) * "rotate(45deg)").bbox()
(-70.71067811865474, 0.0, 0.0, 70.71067811865476)

@tatarize
Copy link
Member

tatarize commented Apr 2, 2021

>> SimpleLine((0,0), (100,100), transform="scale(0.1)").bbox()
(0.0, 0.0, 0.0, 10.0)

And if you wanted to do something like that during init, it's pretty easy to do there too. They will apply according to the svg spec.

>>SimpleLine((0,0), (100,100), transform="scale(0.1) translate(100,200)").bbox()
(10.0, 20.0, 10.0, 30.0)

So the translated location is scaled before bbox() is called. That's true for most things, if SVG said how to do that work, that's how it's done.

As part of #87 I might eventually get around to writing a full reading, writing, and geometric rendering library. But, depending on your uses this one is likely one of the most complete for parsing and remixing of svg geometries.

@ghost
Copy link
Author

ghost commented Apr 2, 2021

Excellent! Thanks for your help!

@ghost
Copy link
Author

ghost commented Apr 2, 2021

By the way, the stroke_width or stroke_linecap seems to have no effect on the bounding box:

In [91]: se.SimpleLine((0,0),(10,10),stroke_linecap="round",stroke_width=100).bbox()
Out[91]: (0.0, 0.0, 0.0, 10.0)

which could be added manually to the bbox for simple shapes like Line, but not for a complex Path!

@tatarize
Copy link
Member

tatarize commented Apr 3, 2021

<svg width="285" height="350" viewBox="0 0 285 350" fill="none" xmlns="http://www.w3.org/2000/svg">
<line id="test-line" x0="0" x1="100" y0="0" y1="100" stroke-width="50" stroke="red"/>
</svg>

Then:

document.getElementById('test-line').getBBox()
> SVGRect {x: 0, y: 0, width: 100, height: 100}

The stroke-width is a paint attribute on the geometry, the getBBox() gets the bounding box of the actual object. If you want your object to actually be a filled fat line within geometry you'd want to convert it to a filled closed shape of the outerpath. In the above test on Chrome you'll see that it had a stroke-width of 50 and thus was well outside the SVGRect() returned but only gave the size of the geometry, which is the correct answer and method.

How things paint and things like their line-cap don't actually define the bounding box. If their actual area is within the bbox() not the total area of the paint.

@tatarize
Copy link
Member

tatarize commented Apr 3, 2021

Also, if you wanted to do this for a path, it wouldn't be that hard. You'd need to do some sampling along the path, finding the normal vector, moving a particular distance from the line on both sides and then use that to define the shape. But, it would be clearly much easier to just add stroke-width to the edges since your value isn't going to exceed that except for perhaps with an line end-cap. svgpathtools has the needed code for finding the tangent and normal vectors for particular paths and even gives an example of offset paths there. It's just that it gets a bit hefty into geometry which isn't really the purpose of the module. Parsing svg solidly and getting the valid geometry is the bulk of the goal here, though that functionality isn't that hard and is still somewhat low-hanging fruit.

@ghost ghost closed this as completed Apr 10, 2021
@ghost
Copy link
Author

ghost commented Apr 22, 2021

<svg width="285" height="350" viewBox="0 0 285 350" fill="none" xmlns="http://www.w3.org/2000/svg">
<line id="test-line" x0="0" x1="100" y0="0" y1="100" stroke-width="50" stroke="red"/>
</svg>

Then:

document.getElementById('test-line').getBBox()
> SVGRect {x: 0, y: 0, width: 100, height: 100}

The stroke-width is a paint attribute on the geometry, the getBBox() gets the bounding box of the actual object. If you want your object to actually be a filled fat line within geometry you'd want to convert it to a filled closed shape of the outerpath. In the above test on Chrome you'll see that it had a stroke-width of 50 and thus was well outside the SVGRect() returned but only gave the size of the geometry, which is the correct answer and method.

How things paint and things like their line-cap don't actually define the bounding box. If their actual area is within the bbox() not the total area of the paint.

Could you maybegive me an example how could I convert a SimpleLine to a filled-closed path so that asking the bbox would consider the stroke_width of the line too?

@tatarize
Copy link
Member

If you wanted to do it with just a line, you could easily get the normal vector and expand the line outwards. Basically you find the direction that is exactly 90° off the direction of the line. You would then expand that shape outwards in both directions by stroke_width/2 and in theory you'd have geometry that would be expected be equal to that line with a stroke width. It wouldn't, however, take into account, endcap with is another property of drawing things where the line gets a end bit, either none or butt or miter etc. But, you would have accounted for the width there.

I don't, however have code to calculate the normal or tangent vectors of a shape. This code does exist within svgpathtools but I didn't manage to port it over or see a reason to do so at the time. This code base is much more concerned with correctly parsing svg and pathtools is more about math. And you'd mostly need to sample the normal vector at certainly positions in order to do this operation. Along a line the correct answer is usually pretty easy. arctan2(y2-y1,x2-x1) + 90° would give you the angle then you could simply use polar coords x = cos(angle) * r + x and y = sin(angle) * r + y and you've calculated the correct angle normal angle of the line and can move any point (x,y) a distance r from that line to find the parallel line. But, there might be easier math to do this and it isn't being helped at all by the library. I'll raise an issue to take normal and tangent angles from svgpathtools but it wouldn't come about very soon.

It gets progressively harder in other shapes but svgpathtools has a demonstration of the offset code there. And while it might be a shame to take the svgelements path and take the .d() value and then put that in an svgpathtools path just to take find a sampling of the normal vector as you need to make an offset curve, it's the only way I currently know how to do right away.

@ghost
Copy link
Author

ghost commented Apr 24, 2021

Many thanks! Still one last question on this: how could I expand the lines pathd outwards?

@ghost
Copy link
Author

ghost commented Apr 24, 2021

I ended up rotating a rect to the angle of the line instead of expanding the shape outwards:

from math import atan2, hypot
import svgwrite as s
import svgelements as se

D = s.drawing.Drawing(filename="/tmp/asd.svg", size=(1000, 1000), debug=True)
x1,y1,x2,y2=10,35,30,20
a=atan2(y2-y1,x2-x1)
thickness =10
ry=y1 - thickness*.5
r=se.Rect(x1,ry,hypot(x2-x1, y2-y1),thickness)*f"rotate({a}rad {x1} {y1})"
# A simpleline to test
D.add(s.path.Path(d=se.SimpleLine(x1,y1,x2,y2).d(), stroke=s.utils.rgb( 0, 0, 205)))
D.add(s.path.Path(d=r.d(),fill=s.utils.rgb(100,0,0,"%")))
D.save(pretty=True)

Screenshot_2021-04-24_17-51-03

This seems to give me what I was looking for.

Thanks for your help again!

@tatarize
Copy link
Member

That's some nifty math. Line is a bit easy to do since your normal vector stays the same, though you get some heftier math with curves. I think a order 3 bezier curve needs to be properly offset with an order 10 curve. Though there's some very simple sampling techniques you can do simply that.

Clever trick replacing the line with a rotated rect of a given thickness. In fact, you could set your rx and ry on the rectangle and make rounded endcaps, which is kind of amusing, since I generally said above that endcaps were a non-starter.

@ghost
Copy link
Author

ghost commented Apr 24, 2021

That's exactly why I wanted the Rect; it looks like a line plus, I have all nice bbox information even by simulating the line-cap with rx/ry.

This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant