5
$\begingroup$

When I load a mesh into a 3D printer slicing program, I get a little wire preview of the tool paths the machine will take.

Since many prints take 8-24 hours or longer, I want to make neat rendered previews of these paths to show clients and friends what a print will look like ahead of time.

I found some old scripts that did this, but they don't work with Blender 2.8 so I tried to update them myself. I know nothing about programming or scripting, so I've done a terrible job, and I need help getting over the last few hurdles.

Here's what I've gotten so far:

3D model generated by GCODE tool paths.

It's a script that allows the user to import a .GCODE file and goes through the text doc line by line placing Curve Points for every command of the 3D printer nozzle.

Then it extrudes an oval along the curves to generate the 3D model.

The two big problems remaining are:

1)

The curves are all separate, straight lines. How can I continue them so instead of ugly stops in the extrusion, it's a more smooth curve?

Extruded ovals on curves with ugly gaps.

The Curve Points are all sharing the same locations and the Curve directions flow properly. Even the shortest Curves are immediately from one spot to another, an unbroken-looking chain, as you'd expect a 3D printer to operate.

I need to either modify the script to make continuous curves or "Merge By Distance" these overlapping Curve Points afterwards, before extruding the oval.

2)

Also, every time you run the script, it appends a new button to the File menu.

Additional script buttons appending into the menu with every Run.

These are definitely rookie problems, but I just can't figure out how to fix them.

Ultimately I am just trying to get a 3D model of the tool paths that I can render nicely in Cycles before physically 3D printing them. If anybody has a better option, please let me know!

Otherwise I need help getting this script working properly and I'm not sure what to do next. Thank you!

E d i t: This is a screenshot out of Cura, a 3D printing slice program. This is the type of results I'd like to see in Blender, for rendering with a nice plastic-looking material on a desk scene. Unfortunately I can't get this model out of a slice program without lots of work.

3D print preview out of CURA

Here is the script so far:

    # ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

## blendish python ripped from fro io_import_dxf

import os
import warnings
import re
import string
import bpy
from bpy.types import Operator
from bpy.utils import register_class
from bpy.utils import unregister_class
import mathutils
from mathutils import Vector
import math
import copy

bl_info = {
    'name': 'Import Slic3r GCode',
    'author': 'Lee Butler, updating (in progress) to 2.8 by Dalton Strong',
    'version': (0,2,0),
    'blender': (2, 80, 0),
    'location': 'File > Import-Export > Gcode',
    'description': 'Import and visualize gcode files generated by Slic3r (.gcode)',
    "wiki_url": "https://github.com/iraytrace/BlenderGcodeImport/wiki",
    "tracker_url": "",
    'category': 'Import-Export'}

__version__ = '.'.join([str(s) for s in bl_info['version']])

class IMPORT_OT_gcode(bpy.types.Operator):
    '''Imports Reprap FDM gcode'''
    bl_idname = "import_scene.gocde"
    bl_description = 'Gcode reader, reads tool moves and animates layer build'
    bl_label = "Import gcode" +' v.'+ __version__
    bl_space_type = "PROPERTIES"
    bl_region_type = "WINDOW"

    filepath = bpy.props.StringProperty(name="File Path", description="Filepath used for importing the GCode file", maxlen= 1024, default= "")

    def __init__(self):
        # current tool position
        self.pos = {'X':0.0, 'Y':0.0, 'Z':0.0, 'E':0.0}

        # set of accumulated points on current extrusion path
        # a set of points makes up a polyline
        self.points = []

        # set of polylines 
        self.polys = []

        # each layer is a set of polylines at a constant Z
        self.layers = []

        # set of Z elevation changes.  Most common is layer height
        self.thickness = { }
        
        self.ySquash = 0.5
        self.xOoze = 1.65


    ##### DRAW #####
    def draw(self, context):
        layout0 = self.layout

    ##### EXECUTE #####
    def execute(self, context):
        print('execute')
        self.parse(self.filepath)
        return {'FINISHED'}

    ##### INVOKE  #####
    def invoke(self, context, event):
        print('invoke')
        wm = context.window_manager
        wm.fileselect_add(self)
        return {'RUNNING_MODAL'}

    ##### PARSE #####
    def parse(self, fileName):
        self.points = []

        #print ('---------- read ' + fileName + ' ------------')
        # get the object/curve names from the filename
        self.obName = fileName.split(os.sep)[-1]
        self.obName = self.obName.replace(".gcode", "")

        f = open(fileName)
        for line in f.readlines():
            # remove comments and leading/trailing whitespace
            line = line.split(';', 1)[0].strip()

            # skip the blank lines
            if len(line) < 1:
                continue

            # tokenize the line
            tokens = line.split()

            self.dispatch(tokens)
        f.close()
        self.newLayer(-1.0)
        
        #print ('---------- build ------------')
        #print (' %d slices' % len(self.layers) )
        #print (' deltaZ values:')
        count = 0
        radius = 0
        # find the most common inter-layer distance
        for key in sorted(self.thickness.keys()):
               #print( '  %s : %d' % (key,self.thickness[key]) )
               if self.thickness[key] > count:
                   count = self.thickness[key]
                   radius = key


        profileName = self.obName + '_profile'
        profileData = bpy.data.curves.new(profileName, type='CURVE')
        profileData.dimensions = '3D'

        profilePoly = profileData.splines.new('POLY')
        profilePoly.points.add(7)
        angRad = radius * 0.70711
        profilePoly.points[7].co = (radius * self.xOoze,  0.0,                 0.0, 1)
        profilePoly.points[6].co = (angRad* self.xOoze,  angRad* self.ySquash, 0.0, 1)
        profilePoly.points[5].co = (0.0,                 radius* self.ySquash, 0.0, 1)
        profilePoly.points[4].co = (-angRad* self.xOoze,  angRad* self.ySquash, 0.0, 1)
        profilePoly.points[3].co = (-radius* self.xOoze,  0.0,                  0.0, 1)
        profilePoly.points[2].co = (-angRad* self.xOoze,  -angRad* self.ySquash, 0.0, 1)
        profilePoly.points[1].co = (0.0,                  -radius* self.ySquash, 0.0, 1)
        profilePoly.points[0].co = (angRad* self.xOoze,  -angRad* self.ySquash, 0.0, 1)
        profilePoly.use_cyclic_u = True
        #print (dir(profilePoly))
        profileObject = bpy.data.objects.new(profileName, profileData)

        scene = bpy.context.scene
        scene.collection.objects.link(profileObject)
        profileObject.select_set(True)


        for layerNum,layer in enumerate(self.layers):

            layerName = self.obName + '_slice_%d' % layerNum
            curveData = bpy.data.curves.new(layerName, type='CURVE')
            curveData.dimensions = '3D'
            curveData.bevel_object = profileObject
            #print (layerName + ':')

            for poly in layer:
                pointNum = 0
                for point in poly:
                    if pointNum == 0:
                        x,y,z = point
                        oldPt = mathutils.Vector((x, y, z, 1))
                        pointNum = 1
                    else:
                        polyline = curveData.splines.new('POLY')
                        polyline.points.add(1)

                        x,y,z = point
                        newPt = mathutils.Vector((x, y, z, 1))
                        polyline.points[0].co = oldPt
                        polyline.points[1].co = newPt
                        oldPt = newPt
            layerObject = bpy.data.objects.new(layerName, curveData)
            scene.collection.objects.link(layerObject)
            bpy.context.view_layer.objects.active = layerObject


        # print('-------------- done -------------')


   
   
    ##### DISPATCH #####
    def dispatch(self, tokens):
        if tokens[0] in dir(self):
            eval('self.' + tokens[0] + '(' + str(tokens[1:]) + ')')
        else:
            print( 'unknown command:' + str(tokens[0]))

    ##### newPoly #####
    def newPoly(self):
        # stash points into curves
        # need to make this a copy
        if len(self.points) > 0:
            #print( 'poly with %d points' % (len(self.points)) )
            #for i,p in enumerate(self.points):
            #    print( '\t %d %s' % (i, str(p)) )
            self.polys.append( self.points[:] )
            self.points = []
    
    ##### newLayer #####
    def newLayer(self, delta):
        # stash existing points into curve
        self.newPoly()

        # stash existing set of polys into layer
        if len(self.polys) > 0:
            #print( 'new layer with %d polys' % len(self.polys) )
            self.layers.append( self.polys[:] )
            
            self.polys = []

            if delta > 0.0 and delta < 1.0:
                if delta in self.thickness.keys():
                    self.thickness[delta] = self.thickness[delta] + 1
                else:
                    self.thickness[delta] = 1
    
    ##### moveTo #####
    def moveTo(self, newPos):
        if newPos['Z'] != self.pos['Z']:
            delta = newPos['Z'] - self.pos['Z']
            self.newLayer(delta)
        
        if newPos['E'] <= self.pos['E'] or newPos['E'] <= 0.0:
            self.newPoly()
        
        if newPos['E'] > 0 and newPos['E'] >= self.pos['E']:
            self.points.append([newPos['X'],
                            newPos['Y'],
                            newPos['Z']])
        
        # should this be an explicit copy?
        self.pos = copy.deepcopy(newPos)


    ###### parseCoords ######
    def parseCoords(self, tokens):
        npos = { }
        for tok in tokens:
            axis = tok[0]
            if axis in ['X', 'Y', 'Z', 'E']:
                npos[axis] = float(tok[1:])
        return npos
    
    ##### parseCoordsUpdate #####
    def parseCoordsUpdate(self, tokens):
        npos = self.parseCoords(tokens)
        
        for axis in ['X', 'Y', 'Z', 'E']:
            if axis not in npos.keys():
                npos[axis] = self.pos[axis]
                
        #print('coord: %8g %8g %8g  %g' % (npos['X'], npos['Y'], npos['Z'], npos['E']))
        return npos
                

    def N(self, tokens):
        '''line number and checksum'''
        # checksum not implemented
        self.dispatch(tokens[1:])

    def G0(self, tokens):
        '''move fast'''

        newPos = self.parseCoordsUpdate(tokens)
        self.moveTo(newPos)
        

    def G1(self, tokens):
        '''move to'''
        self.G0(tokens)

    def G21(self,tokens):
        '''set units mm'''
        pass

    def G28(self, tokens):
        '''move to origin'''
        
        npos = self.pos
        for tok in tokens:
           axis = tok[0]
           if axis in ['X', 'Y', 'Z', 'E']:
               # note that value is ignored
               npos[axis] = 0.0

        # no matter what we won't be extruding
        npos['E'] = 0.0
        self.moveTo(npos)

    def G90(self, tokens):
        '''set absolute positioning'''
        pass

    def G92(self, tokens):
        '''set position'''
        # fortunately Slic3r does not set arbitrary coordinates
        # or do relative positioning.  This is used just to zero
        # out the position on the extruder.

        newPos = self.parseCoordsUpdate(tokens)
        if newPos['E'] == 0:
            self.newPoly()

        self.pos = newPos

    def M82(self, tokens):
        '''set extruder absolute mode'''
        pass

    def M84(self, tokens):
        '''stop idle hold'''
        pass

    def M104(self,tokens):
        '''set extruder temperature'''
        pass

    def M106(self, tokens):
        '''fan on'''
        pass

    def M107(self,tokens):
        '''fan off'''
        pass

    def M109(self,tokens):
        '''set extruder temperature and wait'''
        pass

# ------------------------
# Registering

def menu_func(self, context):
    self.layout.operator(IMPORT_OT_gcode.bl_idname, text="GCode (.gcode)", icon='TEXTURE')

classes = (
    IMPORT_OT_gcode,
)

def register():
    from bpy.utils import register_class
    for cls in classes:
        register_class(cls)
    bpy.types.TOPBAR_MT_file_import.append(menu_func)
        
def unregister():
    from bpy.utils import unregister_class
    for cls in classes:
        unregister_class(cls)
    bpy.types.TOPBAR_MT_file_import.remove(menu_func)

if __name__ == "__main__":
    register()
    
    
$\endgroup$
2
  • 1
    $\begingroup$ Instead of recreating all the print paths, you can also just use the original model and add a shader to it making it look 3d printed. For example, here: blender.stackexchange.com/questions/6210/…. $\endgroup$
    – Eric Xue
    Commented Sep 1, 2020 at 19:18
  • $\begingroup$ @EricXue That's what I've been doing before looking into this. It's a good way to do it, but not quite as high quality as what I could get with actual geometry. Also the pattern of the extrusion on the top surface of the printed model changes and I'd like the render to show that. $\endgroup$ Commented Sep 4, 2020 at 14:25

1 Answer 1

6
$\begingroup$

There is a G-code importer addon for Blender 2.8 here.
https://github.com/Heinz-Loepmeier/Blender-Gcode-Import
The beveling/meshing of the curve is best done with Blenders internal curve editing tools.
You can choose a custom bevel profile there, that will get rid of unconnected geometry around the corners. enter image description here

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .