massive update for STL Input extension

This commit is contained in:
leyghisbb 2021-05-15 02:37:35 +02:00
parent 02635a5982
commit da2023d139
2 changed files with 144 additions and 90 deletions

View File

@ -3,43 +3,80 @@
<name>Slic3r STL Input</name> <name>Slic3r STL Input</name>
<id>fablabchemnitz.de.input_stl</id> <id>fablabchemnitz.de.input_stl</id>
<param name="tab" type="notebook"> <param name="tab" type="notebook">
<page name="options" gui-text="Options"> <page name="tab_options" gui-text="Options">
<label appearance="header">Processor</label>
<param name="slic3r_cmd" type="path" mode="file" gui-text="Slic3r command">/path/to/slic3r/v1.3.1-dev</param>
<label appearance="header">Input</label>
<param name="inputfile" type="path" gui-text="Input file" filetypes="obj,off,ply,stl" mode="file">/your/object/file.stl</param> <param name="inputfile" type="path" gui-text="Input file" filetypes="obj,off,ply,stl" mode="file">/your/object/file.stl</param>
<param name="layer_height" type="string" gui-text="Layer height [mm]:">1.0</param> <param name="max_num_faces" type="int" min="1" max="99999" gui-text="Maximum allowed faces" gui-description="If the STL file has too much detail it contains a large number of faces. This will make processing extremely slow. So we can limit it.">200</param>
<param name="rx" type="float" precision="1" min="-360.0" max="360.0" gui-text="3D-Rotate X-Axis [deg]:">0</param> <param name="layer_height" type="string" min="0.001" max="99999.000" precision="3" gui-text="Layer height [mm]:">1.000</param>
<param name="ry" type="float" precision="1" min="-360.0" max="360.0" gui-text="3D-Rotate Y-Axis [deg]:">0</param> <hbox>
<param name="numbers" type="bool" gui-text="Add layer numbers">false</param> <vbox>
<param name="center" type="bool" gui-text="Add center marks">false</param> <label appearance="header">Transforms</label>
<param name="stroke_width" type="float" min="0" max="100" gui-text="Stroke width (px):">2</param> <param name="scalefactor" type="float" precision="3" min="0.0001" max="10000.0" gui-text="Manual scale factor" gui-description="default is 1.0">1.0</param>
<param name="path_color" type="color" appearance="colorbutton" gui-text="Path color">879076607</param> <param name="rx" type="float" precision="1" min="-360.0" max="360.0" gui-text="3D-Rotate X-Axis [deg]:">0</param>
<param name="fill_color" type="color" appearance="colorbutton" gui-text="Fill color">1943148287</param> <param name="ry" type="float" precision="1" min="-360.0" max="360.0" gui-text="3D-Rotate Y-Axis [deg]:">0</param>
<param name="use_fill_color" type="bool" gui-text="Use fill color">false</param> <param name="rz" type="float" precision="1" min="-360.0" max="360.0" gui-text="3D-Rotate Z-Axis [deg]:">0</param>
<param name="tone_down" type="optiongroup" appearance="combo" gui-text="Town down opacity for each layer"> </vbox>
<option value="regular">regular</option> <separator/>
<option value="front_to_back">front to back</option> <vbox>
<option value="back_to_front">back to front</option> <label appearance="header">Output</label>
</param> <param name="resizetoimport" type="bool" gui-text="Resize the canvas to the imported drawing's bounding box">true</param>
<param name="extraborder" type="float" precision="3" gui-text="Add extra border around fitted canvas">0.0</param>
<param name="extraborder_units" type="optiongroup" appearance="combo" gui-text="Border offset units">
<option value="mm">mm</option>
<option value="cm">cm</option>
<option value="in">in</option>
<option value="pt">pt</option>
<option value="px">px</option>
</param>
<param name="numbers" type="bool" gui-text="Add layer numbers">false</param>
<param name="center" type="bool" gui-text="Add center marks">false</param>
</vbox>
<separator/>
<vbox>
<label appearance="header">Style</label>
<param name="stroke_width" type="float" min="0" max="100" gui-text="Stroke width (px):">2</param>
<param name="path_color" type="color" appearance="colorbutton" gui-text="Path color">879076607</param>
<param name="use_path_color" type="bool" gui-text="Use path color">true</param>
<param name="fill_color" type="color" appearance="colorbutton" gui-text="Fill color">1943148287</param>
<param name="use_fill_color" type="bool" gui-text="Use fill color">false</param>
<param name="tone_down" type="optiongroup" appearance="combo" gui-text="Town down opacity for each layer">
<option value="regular">regular</option>
<option value="front_to_back">front to back</option>
<option value="back_to_front">back to front</option>
</param>
</vbox>
</hbox>
</page> </page>
<page name="slic3r" gui-text="Slic3r Settings"> <page name="tab_about" gui-text="About">
<param name="slic3r_cmd" type="path" mode="file" gui-text="Slic3r Command">slic3r</param> <label appearance="header">Slic3r STL Input</label>
<label xml:space="preserve">The slic3r command name depends on your operating system, and how slic3r was installed. <label>This extension is highly based on the work of Jürgen Weigert. It projects an STL file on the X-Y plane by cutting the objects into "slices". Each "slice" is a group of polygons, with a label indicating it's z position. The polygons are converted to paths for better editing in inkscape. Use Object -&gt; Rows &amp; Columns to distribute the slices in a grid. Possible input files are STL, Wavefront OBJ, PLY and OFF.</label>
Typical values are: <label>2021 / written by Mario Voigt (Stadtfabrikanten e.V. / FabLab Chemnitz)</label>
<spacer/>
$HOME/Downloads/Slic3r-1.3.0-x86_64.AppImage (Linux) <label appearance="header">Online Documentation</label>
%USERPROFILE%/Slic3r/Slic3r-console.exe (Windows)</label> <label appearance="url">https://y.stadtfabrikanten.org/slic3rstlinput</label>
<spacer/>
<label appearance="header">Contributing</label>
<label appearance="url">https://gitea.fablabchemnitz.de/MarioVoigt/mightyscape-1.X</label>
<label appearance="url">mailto:mario.voigt@stadtfabrikanten.org</label>
<spacer/>
<label appearance="header">Third Party Modules</label>
<label appearance="url">https://slic3r.org/download</label>
<spacer/>
<label appearance="header">MightyScape Extension Collection</label>
<label>This piece of software is part of the MightyScape for Inkscape Extension Collection and is licensed under GNU GPL v3</label>
<label appearance="url">https://y.stadtfabrikanten.org/mightyscape-overview</label>
</page> </page>
<page name="help" gui-text="Help"> <page name="tab_donate" gui-text="Donate">
<label xml:space="preserve">- Projects an STL file on the X-Y plane by cutting the objects into "slices". <label appearance="header">Coffee + Pizza</label>
- Each "slice" is a group of polygons, with a label indicating its z position. <label>We are the Stadtfabrikanten, running the FabLab Chemnitz since 2016. A FabLab is an open workshop that gives people access to machines and digital tools like 3D printers, laser cutters and CNC milling machines.</label>
- The polygons are converted to paths for better editing in inkscape. <spacer/>
<label>You like our work and want to support us? You can donate to our non-profit organization by different ways:</label>
- Use Object -&gt; Rows &amp; Columns ... distribute the slices in a grid.</label> <label appearance="url">https://y.stadtfabrikanten.org/donate</label>
<spacer/>
<label appearance="url">https://github.com/jnweiger/inkscape-input-stl</label> <label>Thanks for using our extension and helping us!</label>
<label xml:space="preserve">(C) 2018 by Jürgen Weigert &lt;jnweiger@gmail.com&gt; and others. <image>000_about_fablabchemnitz.svg</image>
Distribute under GPLv2 or ask.</label>
<label appearance="url">https://slic3r.org/download</label>
</page> </page>
</param> </param>
<effect> <effect>

View File

@ -23,9 +23,15 @@ For optional(!) rotation support:
- moved extension to sub menu structure (allows preview) - moved extension to sub menu structure (allows preview)
- added different options - added different options
#Notes
* requires exactly Slic3r-1.3.1-dev
-> https://dl.slic3r.org/dev/linux/
-> https://dl.slic3r.org/dev/win/
* Make it available through PrusaSlic3r? > this is not possible because no support to slice into SVG layers
#ToDos #ToDos
* Make it available through PrusaSlic3r
* FIXME: should use svg_pathstats(path_d): to compute bounding boxes. * FIXME: should use svg_pathstats(path_d): to compute bounding boxes.
''' '''
import sys import sys
import os import os
@ -34,12 +40,10 @@ import subprocess
from lxml import etree from lxml import etree
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
import inkex import inkex
from inkex import Color from inkex import Color, Transform
import tempfile import tempfile
import openmesh as om import openmesh as om
import stl #numpy-stl lib
import numpy
import math
sys_platform = sys.platform.lower() sys_platform = sys.platform.lower()
if sys_platform.startswith('win'): if sys_platform.startswith('win'):
@ -47,7 +51,7 @@ if sys_platform.startswith('win'):
elif sys_platform.startswith('darwin'): elif sys_platform.startswith('darwin'):
slic3r = 'slic3r' slic3r = 'slic3r'
else: # Linux else: # Linux
slic3r = os.environ['HOME']+ '/Downloads/Slic3r-1.3.0-x86_64.AppImage' slic3r = os.environ['HOME']+ '/Downloads/Slic3r-1.3.1-dev-x86_64.AppImage'
if not os.path.exists(slic3r): if not os.path.exists(slic3r):
slic3r = 'slic3r' slic3r = 'slic3r'
@ -57,13 +61,20 @@ class InputSTL(inkex.EffectExtension):
pars.add_argument('--tab') pars.add_argument('--tab')
pars.add_argument('--inputfile', help='STL input file to convert to SVG with the same name, but ".svg" suffix.') pars.add_argument('--inputfile', help='STL input file to convert to SVG with the same name, but ".svg" suffix.')
pars.add_argument('-l', '--layer_height', default=None, help='slic3r layer height, probably in mm. Default: per slic3r config') pars.add_argument('--scalefactor', type=float, default=1.0, help='Scale the model to custom size')
pars.add_argument('--rx', default=None, type=float, help='Rotate STL object around X-Axis before importing.') pars.add_argument("--max_num_faces", type=int, default=200, help="If the STL file has too much detail it contains a large number of faces. This will make processing extremely slow. So we can limit it.")
pars.add_argument('--ry', default=None, type=float, help='Rotate STL object around Y-Axis before importing.') pars.add_argument('-l', '--layer_height', type=float, default=1.000, help='slic3r layer height, probably in mm. Default: per slic3r config')
pars.add_argument('--rx', type=float, default=None, help='Rotate STL object around X-Axis before importing.')
pars.add_argument('--ry', type=float, default=None, help='Rotate STL object around Y-Axis before importing.')
pars.add_argument('--rz', type=float, default=None, help='Rotate STL object around Z-Axis before importing.')
pars.add_argument('--numbers', type=inkex.Boolean, default=False, help='Add layer numbers.') pars.add_argument('--numbers', type=inkex.Boolean, default=False, help='Add layer numbers.')
pars.add_argument('--center', type=inkex.Boolean, default=False, help='Add center marks.') pars.add_argument('--center', type=inkex.Boolean, default=False, help='Add center marks.')
pars.add_argument("--resizetoimport", type=inkex.Boolean, default=True, help="Resize the canvas to the imported drawing's bounding box")
pars.add_argument("--extraborder", type=float, default=0.0)
pars.add_argument("--extraborder_units")
pars.add_argument("--stroke_width", type=float, default=2.0, help="Stroke width (px)") pars.add_argument("--stroke_width", type=float, default=2.0, help="Stroke width (px)")
pars.add_argument('--path_color', type=Color, default='879076607', help="Path color") pars.add_argument('--path_color', type=Color, default='879076607', help="Path color")
pars.add_argument("--use_path_color", type=inkex.Boolean, default=True, help="Use path color")
pars.add_argument("--fill_color", type=Color, default='1943148287', help="Fill color") pars.add_argument("--fill_color", type=Color, default='1943148287', help="Fill color")
pars.add_argument("--use_fill_color", type=inkex.Boolean, default=False, help="Use fill color") pars.add_argument("--use_fill_color", type=inkex.Boolean, default=False, help="Use fill color")
pars.add_argument("--tone_down", default="regular", help="Town down opacity for each layer") pars.add_argument("--tone_down", default="regular", help="Town down opacity for each layer")
@ -72,41 +83,13 @@ class InputSTL(inkex.EffectExtension):
def effect(self): def effect(self):
args = self.options args = self.options
inputfile = args.inputfile
outputfilebase = os.path.splitext(os.path.basename(inputfile))[0]
converted_inputfile = os.path.join(tempfile.gettempdir(), outputfilebase + ".stl")
if not os.path.exists(inputfile):
inkex.utils.debug("The input file does not exist.")
exit(1)
om.write_mesh(converted_inputfile, om.read_trimesh(inputfile)) #read + convert # might throw "[STLWriter] : Warning non-triangle data!"
args.inputfile = converted_inputfile #overwrite
# input-stl.inx advertises use of '$HOME' -- windows has HOMEPATH instead of HOME
home = os.environ.get('HOME', os.environ.get('HOMEPATH', 'NO-HOME'))
#args.slic3r_cmd = re.sub('^\$HOME(PATH)?', home, args.slic3r_cmd)
#############################################
# test slic3r command
#############################################
if sys_platform.startswith('win'): if sys_platform.startswith('win'):
# assert we run the commandline version of slic3r # assert we run the commandline version of slic3r
args.slic3r_cmd = re.sub('slic3r(\.exe)?$', 'slic3r-console.exe', args.slic3r_cmd, flags=re.I) args.slic3r_cmd = re.sub('slic3r(\.exe)?$', 'slic3r-console.exe', args.slic3r_cmd, flags=re.I)
tmp_inputfile = None
if args.rx is not None and abs(args.rx) < 0.01: args.rx = None
if args.ry is not None and abs(args.ry) < 0.01: args.ry = None
if args.rx or args.ry:
try:
mesh = stl.Mesh.from_file(inputfile)
if args.rx: mesh.rotate([1.0, 0.0, 0.0], math.radians(float(args.rx)))
if args.ry: mesh.rotate([0.0, 1.0, 0.0], math.radians(float(args.ry)))
inputfile = tmp_inputfile = tempfile.gettempdir() + os.path.sep + 'ink-stl-' + str(os.getpid()) + '.stl'
mesh.save(inputfile)
except Exception as e:
inkex.utils.debug("Rotate failed: " + str(e))
svgfile = re.sub('\.stl', '.svg', args.inputfile, flags=re.IGNORECASE)
cmd = [args.slic3r_cmd, '--version'] cmd = [args.slic3r_cmd, '--version']
try: try:
proc = subprocess.Popen(cmd, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) proc = subprocess.Popen(cmd, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
@ -116,18 +99,35 @@ class InputSTL(inkex.EffectExtension):
sys.exit(1) sys.exit(1)
stdout, stderr = proc.communicate() stdout, stderr = proc.communicate()
#############################################
# prepare STL input (also accept and convert obj, stl, ply, off)
#############################################
outputfilebase = os.path.splitext(os.path.basename(args.inputfile))[0]
converted_inputfile = os.path.join(tempfile.gettempdir(), outputfilebase + ".stl")
if not os.path.exists(args.inputfile):
inkex.utils.debug("The input file does not exist.")
exit(1)
input_mesh = om.read_trimesh(args.inputfile)
if input_mesh.n_faces() > args.max_num_faces:
inkex.utils.debug("Aborted. Target STL file has " + str(input_mesh.n_faces()) + " faces, but only " + str(args.max_num_faces) + " are allowed.")
exit(1)
om.write_mesh(converted_inputfile, input_mesh) #read + convert, might throw errors; warning. output is ASCII but cannot controlled to be set to binary because om.Options() is not available in python binding yet
if not os.path.exists(converted_inputfile):
inkex.utils.debug("The converted input file does not exist.")
exit(1)
args.inputfile = converted_inputfile #overwrite
#############################################
# create the layer slices
#############################################
svgfile = re.sub('\.stl', '.svg', args.inputfile, flags=re.IGNORECASE)
# option --layer-height does not work. We use --scale instead... # option --layer-height does not work. We use --scale instead...
scale = 1/float(args.layer_height) scale = 1.0 / args.layer_height
cmd = [args.slic3r_cmd, '--no-gui'] cmd = [args.slic3r_cmd, '--no-gui', '--scale', str(scale), '--rotate-x', str(args.rx), '--rotate-y', str(args.ry), '--rotate', str(args.rz), '--first-layer-height', '0.1mm', '--export-svg', '-o', svgfile, args.inputfile]
if args.layer_height is not None:
cmd += ['--scale', str(scale), '--first-layer-height', '0.1mm'] # args.layer_height+'mm']
cmd += ['--export-svg', '-o', svgfile, inputfile]
magic = 10 # layer width seems to be 0.1mm ??? magic = 10 # layer width seems to be 0.1mm ???
def scale_points(pts, scale): def scale_points(pts, scale):
""" str='276.422496,309.4 260.209984,309.4 260.209984,209.03 276.422496,209.03' """ str='276.422496,309.4 260.209984,309.4 260.209984,209.03 276.422496,209.03' """
"""
return re.sub('\d*\.\d*', lambda x: str(float(x.group(0))*scale*magic), pts) return re.sub('\d*\.\d*', lambda x: str(float(x.group(0))*scale*magic), pts)
@ -203,7 +203,7 @@ class InputSTL(inkex.EffectExtension):
return bb return bb
if args.center is not False: if args.center is not False:
bb = bbox_info(args.slic3r_cmd, inputfile) bb = bbox_info(args.slic3r_cmd, args.inputfile)
# Ouch: bbox info gives us stl coordinates. slic3r translates them into svg px using 75dpi. # Ouch: bbox info gives us stl coordinates. slic3r translates them into svg px using 75dpi.
cx = (-bb['min_x'] + bb['max_x']) * 0.5 * 1/scale * magic * 25.4 / 75 cx = (-bb['min_x'] + bb['max_x']) * 0.5 * 1/scale * magic * 25.4 / 75
cy = (-bb['min_y'] + bb['max_y']) * 0.5 * 1/scale * magic * 25.4 / 75 cy = (-bb['min_y'] + bb['max_y']) * 0.5 * 1/scale * magic * 25.4 / 75
@ -214,9 +214,6 @@ class InputSTL(inkex.EffectExtension):
raise OSError("{0}\nCommand failed: errno={1} {2}".format(' '.join(cmd), e.errno, e.strerror)) raise OSError("{0}\nCommand failed: errno={1} {2}".format(' '.join(cmd), e.errno, e.strerror))
stdout, stderr = proc.communicate() stdout, stderr = proc.communicate()
if tmp_inputfile and os.path.exists(tmp_inputfile):
os.unlink(tmp_inputfile)
if not b'Done.' in stdout: if not b'Done.' in stdout:
inkex.utils.debug("Command failed: {0}".format(' '.join(cmd))) inkex.utils.debug("Command failed: {0}".format(' '.join(cmd)))
inkex.utils.debug("OUT: " + str(stdout)) inkex.utils.debug("OUT: " + str(stdout))
@ -280,20 +277,25 @@ class InputSTL(inkex.EffectExtension):
for e in doc.iterfind('//{*}polygon'): for e in doc.iterfind('//{*}polygon'):
polygoncount += 1 polygoncount += 1
if args.tone_down == "front_to_back": if args.tone_down == "front_to_back":
stroke_and_fill_opacity = polygoncount / totalPolygoncount stroke_and_fill_opacity = 1 - (polygoncount / totalPolygoncount)
elif args.tone_down == "back_to_front": elif args.tone_down == "back_to_front":
stroke_and_fill_opacity = 1 - (polygoncount / totalPolygoncount) stroke_and_fill_opacity = polygoncount / totalPolygoncount
elif args.tone_down == "regular": elif args.tone_down == "regular":
stroke_and_fill_opacity = 1.0 stroke_and_fill_opacity = 1.0
else: else:
inkex.utils.debug("Error: unkown town down option") inkex.utils.debug("Error: unkown town down option")
exit(1) exit(1)
if args.use_path_color is False:
stroke = ""
else:
stroke = "stroke:{}".format(args.path_color)
# e.tag = '{http://www.w3.org/2000/svg}polygon' # e.tag = '{http://www.w3.org/2000/svg}polygon'
# e.attrib = {'{http://slic3r.org/namespaces/slic3r}type': 'contour', 'points': '276.422496,309.4 260.209984,309.4 260.209984,209.03 276.422496,209.03', 'style': 'fill: white'} # e.attrib = {'{http://slic3r.org/namespaces/slic3r}type': 'contour', 'points': '276.422496,309.4 260.209984,309.4 260.209984,209.03 276.422496,209.03', 'style': 'fill: white'}
e.tag = re.sub('polygon$', 'path', e.tag) e.tag = re.sub('polygon$', 'path', e.tag)
e.attrib['id'] = 'polygon%d' % polygoncount e.attrib['id'] = 'polygon%d' % polygoncount
e.attrib['{http://www.inkscape.org/namespaces/inkscape}connector-curvature'] = '0' e.attrib['{http://www.inkscape.org/namespaces/inkscape}connector-curvature'] = '0'
e.attrib['style'] = 'fill:{};fill-opacity:{};stroke:{};stroke-opacity:{};stroke-width:{}'.format(fill, stroke_and_fill_opacity, args.path_color, stroke_and_fill_opacity, args.stroke_width) e.attrib['style'] = 'fill:{};fill-opacity:{};{};stroke-opacity:{};stroke-width:{}'.format(fill, stroke_and_fill_opacity, stroke, stroke_and_fill_opacity, args.stroke_width)
e.attrib['d'] = 'M ' + re.sub(' ', ' L ', scale_points(e.attrib['points'], 1/scale)) + ' Z' e.attrib['d'] = 'M ' + re.sub(' ', ' L ', scale_points(e.attrib['points'], 1/scale)) + ' Z'
del e.attrib['points'] del e.attrib['points']
if e.attrib.get('{http://slic3r.org/namespaces/slic3r}type') == 'contour': if e.attrib.get('{http://slic3r.org/namespaces/slic3r}type') == 'contour':
@ -310,5 +312,20 @@ class InputSTL(inkex.EffectExtension):
for element in doc.getroot().iter("{http://www.w3.org/2000/svg}g"): for element in doc.getroot().iter("{http://www.w3.org/2000/svg}g"):
stl_group.append(element) stl_group.append(element)
#apply scale factor
translation_matrix = [[args.scalefactor, 0.0, 0.0], [0.0, args.scalefactor, 0.0]]
stl_group.transform = Transform(translation_matrix) * stl_group.transform
#adjust canvas to the inserted unfolding
if args.resizetoimport:
bbox = stl_group.bounding_box()
namedView = self.document.getroot().find(inkex.addNS('namedview', 'sodipodi'))
root = self.svg.getElement('//svg:svg');
offset = self.svg.unittouu(str(args.extraborder) + args.extraborder_units)
root.set('viewBox', '%f %f %f %f' % (bbox.left - offset, bbox.top - offset, bbox.width + 2 * offset, bbox.height + 2 * offset))
root.set('width', "{:0.6f}{}".format(bbox.width + 2 * offset, self.svg.unit))
root.set('height', "{:0.6f}{}".format(bbox.height + 2 * offset, self.svg.unit))
if __name__ == '__main__': if __name__ == '__main__':
InputSTL().run() InputSTL().run()