FullControlXYZ / fullcontrol

Python version of FullControl for toolpath design (and more) - the readme below is best source of information
GNU General Public License v3.0
672 stars 78 forks source link

SVG import #11

Closed dicksondickson closed 1 year ago

dicksondickson commented 1 year ago

SVG import function will be awesome. I want to integrate typography and other shapes to use in FC to create some cool designs.

I think this will be very useful for those using FC with a laser cutter, pen plotter, and vinyl cutter(?).

Thank you!

fullcontrol-xyz commented 1 year ago

I've made a gist to do this

You just need to add a few lines to open an svg file instead of writing the svg string directly in python code (and to upload the file to google colab if using it)

image

Let me know if this does what you need or if there are any problems. If it seems like the function works well after a couple of people have commented, I'll add it to FullControl lab

dicksondickson commented 1 year ago

This is awesome thank you! As a designer this function opens up more options for me.

The code works, but the paths are flipped like this:

Screenshot 2023-04-28 231119

ChatGPT helped me modify the code and now the results are as expected:

Screenshot 2023-04-28 231240

Here is the modified code:

def get_point_at(path, distance, scale, offset, height):
    pos = path.point(distance)
    pos += offset
    pos *= scale
    return pos.real, height - pos.imag  # invert the y-coordinate

def points_from_path(path, density, scale, offset, height):
    step = int(path.length() * density)
    last_step = step - 1

    if last_step == 0:
        yield get_point_at(path, 0, scale, offset, height)
        return

    for distance in range(step):
        yield get_point_at(path, distance / last_step, scale, offset, height)

def points_from_doc(doc, density=5, scale=1, offset=0):
    height = float(doc.getElementsByTagName("svg")[0].getAttribute("viewBox").split()[3])
    offset = offset[0] + offset[1] * 1j
    points = []
    for element in doc.getElementsByTagName("path"):
        for path in parse_path(element.getAttribute("d")):
            points.extend(points_from_path(path, density, scale, offset, height))

    return points

def svg_to_points(svg_str: str, z_height: float) -> list:
    doc = minidom.parseString(svg_str)
    points = points_from_doc(doc, density=1, scale=5, offset=(0, 5))
    doc.unlink()
    return [fc.Point(x=pt[0], y=pt[1], z=z_height) for pt in points]

And for reading svg file in the notebook:

svgFile = "type.svg"

with open(svgFile, 'rb') as f:
    svg = f.read().decode('utf-8')
fullcontrol-xyz commented 1 year ago

Ah brilliant!! I'll update the original gist when I can. I can see there are lines connecting the letters in the printpath preview in your post. Presumably those lines don't appear in the SVG itself? I'll try to find a way to set these as travel. Should be simple. Can you share your svg as an attachment to reply to this? Or as a gist or something?

dicksondickson commented 1 year ago

Presumably those lines don't appear in the SVG itself? I'll try to find a way to set these as travel.

Yes you are correct, each letter should be standalone and not connect. Thank you so much!

Here is the SVG file:

type

I think this will SVG import example will serve as a good reference for newcomers to FC.

fullcontrol-xyz commented 1 year ago

Thank you. In terms of size, is that svg somewhat 'normal'? Did you choose size or anything? I can see it's big on the print bed (as was my example) but want to make the default values in the function reasonable for desktop printing at maybe 1/10th the size of our examples

dicksondickson commented 1 year ago

Now that you mentioned it, the size is definitely not correct. The width of the text should be 116mm. I created the SVG in Adobe Illustrator and exported as SVG. Do you think Illustrator is exporting the SVG at an incorrect scale?

dicksondickson commented 1 year ago

In Illustrator I set my units to mm. When exporting to SVG, it converts to some unknown scale.

I changed my units to pixels and now the exported SVG viewbox corresponds to the width of the text (116px) but in the notebook plot, the size is still too big.

Here is the new updated SVG:

type_px

dicksondickson commented 1 year ago

Fixed and working as expected. Cleaned up code and moved vars to top so its more clear.

Here is the updated code (also posted this in the gist):

# derived from https://stackoverflow.com/questions/69313876/how-to-get-points-of-the-svg-paths

from svg.path import parse_path
from xml.dom import minidom

density = 2 # set point density
scale = 1 # set SVG scale factor
offset = (0,0) # set SVG coordinate offset

def get_point_at(path, distance, scale, offset, height):
    pos = path.point(distance)
    pos += offset
    pos *= scale
    return pos.real, height - pos.imag  # invert the y-coordinate

def points_from_path(path, density, scale, offset, height):
    step = int(path.length() * density)
    last_step = step - 1

    if last_step == 0:
        yield get_point_at(path, 0, scale, offset, height)
        return

    for distance in range(step):
        yield get_point_at(path, distance / last_step, scale, offset, height)

def points_from_doc(doc, density, scale, offset):
    height = float(doc.getElementsByTagName("svg")[0].getAttribute("viewBox").split()[3])
    offset = offset[0] + offset[1] * 1j
    points = []
    for element in doc.getElementsByTagName("path"):
        for path in parse_path(element.getAttribute("d")):
            points.extend(points_from_path(path, density, scale, offset, height))

    return points

def svg_to_points(svg_str: str, z_height: float) -> list:
    doc = minidom.parseString(svg_str)
    points = points_from_doc(doc, density, scale, offset)
    doc.unlink()
    return [fc.Point(x=pt[0], y=pt[1], z=z_height) for pt in points]

Scale var was set differently in 2 places.

So the workflow is, in whatever program you are creating the SVG, work in pixel units, export/save as SVG then load into FC.

Screenshot 2023-04-30 002854

fullcontrol-xyz commented 1 year ago

Great stuff! I expect people will use lots of different svg-generation software, with a range of potential size-conversion strategies, so I'll make it easy to adjust scale when calling the function in FullControl. I'll also do some tweaks to make sure the code can be imported nicely when using FullControl in jupyter notebooks and in plain python scripts

dicksondickson commented 1 year ago

Awesome thank you!

ES-Alexander commented 1 year ago

@dicksondickson neat idea - I'm sure this will be useful to lots of people! :-)

Just an FYI, you can get GitHub to format your code nicely by specifying the language at the start of the code-block, e.g.

```python
def function(var):
    pass # do something
becomes
```python
def function(var):
    pass # do something
ES-Alexander commented 1 year ago

These icons may be of interest once SVG import is available, and could potentially even be built in as selectable via an interface / inputable via a shortcode or similar.

fullcontrol-xyz commented 1 year ago

added to roadmap - closing