├── README.md ├── __init__.py ├── gcode_export.py └── utils.py /README.md: -------------------------------------------------------------------------------- 1 | # Gcode Exporter 2 | 3 | Export edges paths or polylines as gcode files for digital fabrication 4 | Gcode Exporter is not a slicer, just a tool that allows to export a custom path as gocode file in order to use it for digital fabrication purposes (e.g. 3D Printing) 5 | 6 | ### Installation: 7 | 8 | 1. Start Blender. Open User Preferences, the addons tab 9 | 2. Click "install from file" and point Blender at the downloaded zip ("Install..." for Blender 2.80) 10 | 3. Activate Gcode-Exporter add-on from user preferences 11 | 4. Save user preferences if you want to have it on at startup. (This could be not necessary for Blender 2.80 if "Auto-Save Preferences" id on) 12 | 13 | ### Documentation 14 | 15 | Documentation: https://github.com/alessandro-zomparelli/gcode-exporter/wiki 16 | 17 | 18 | ### Contribute 19 | Please help me keeping Gcode Exporter stable and updated, report any issue here: https://github.com/alessandro-zomparelli/gcode-exporter/issues 20 | 21 | Gcode Exporter is free and open-source. I really think that this is the power of Blender and I wanted to give my small contribution to it. 22 | If you like my work and you want to help to continue the development of Gcode Exporter, please consider to make a small donation. Any small contribution is really appreciated, thanks! :-D 23 | 24 | Alessandro 25 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | # ----------------------------- GCODE EXPORTER ------------------------------- # 20 | # # 21 | # Alessandro Zomparelli # 22 | # (2020) # 23 | # # 24 | # http://www.co-de-it.com/ # 25 | # http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Mesh/Tissue # 26 | # # 27 | # ############################################################################ # 28 | 29 | bl_info = { 30 | "name": "Gcode Exporter", 31 | "author": "Alessandro Zomparelli (Co-de-iT)", 32 | "version": (0, 1, 00), 33 | "blender": (2, 80, 0), 34 | "location": "", 35 | "description": "Export edges paths or polylines as gcode files for digital fabrication", 36 | "warning": "", 37 | "wiki_url": "https://github.com/alessandro-zomparelli/gcode_exporter/wiki", 38 | "tracker_url": "https://github.com/alessandro-zomparelli/gcode_exporter/issues", 39 | "category": "Import-Export"} 40 | 41 | 42 | if "bpy" in locals(): 43 | import importlib 44 | importlib.reload(utils) 45 | importlib.reload(gcode_export) 46 | 47 | else: 48 | from . import utils 49 | from . import gcode_export 50 | 51 | import bpy 52 | from bpy.props import PointerProperty, CollectionProperty, BoolProperty 53 | 54 | classes = ( 55 | gcode_export.GCODE_PT_gcode_exporter, 56 | gcode_export.gcode_settings, 57 | gcode_export.gcode_export 58 | ) 59 | 60 | def register(): 61 | from bpy.utils import register_class 62 | for cls in classes: 63 | bpy.utils.register_class(cls) 64 | bpy.types.Scene.gcode_settings = PointerProperty( 65 | type=gcode_export.gcode_settings 66 | ) 67 | 68 | def unregister(): 69 | from bpy.utils import unregister_class 70 | for cls in classes: 71 | bpy.utils.unregister_class(cls) 72 | 73 | 74 | if __name__ == "__main__": 75 | register() 76 | -------------------------------------------------------------------------------- /gcode_export.py: -------------------------------------------------------------------------------- 1 | import bpy, os 2 | import numpy as np 3 | import mathutils 4 | from mathutils import Vector 5 | from math import pi 6 | from bpy.types import ( 7 | Operator, 8 | Panel, 9 | PropertyGroup, 10 | ) 11 | from bpy.props import ( 12 | BoolProperty, 13 | EnumProperty, 14 | FloatProperty, 15 | IntProperty, 16 | StringProperty, 17 | PointerProperty 18 | ) 19 | from .utils import * 20 | 21 | def change_speed_mode(self, context): 22 | props = context.scene.gcode_settings 23 | if props.previous_speed_mode != props.speed_mode: 24 | if props.speed_mode == 'SPEED': 25 | props.speed = props.feed/60 26 | props.speed_vertical = props.feed_vertical/60 27 | props.speed_horizontal = props.feed_horizontal/60 28 | else: 29 | props.feed = props.speed*60 30 | props.feed_vertical = props.speed_vertical*60 31 | props.feed_horizontal = props.speed_horizontal*60 32 | props.previous_speed_mode == props.speed_mode 33 | return 34 | 35 | class gcode_settings(PropertyGroup): 36 | last_e : FloatProperty(name="Pull", default=5.0, min=0, soft_max=10) 37 | path_length : FloatProperty(name="Pull", default=5.0, min=0, soft_max=10) 38 | 39 | folder : StringProperty( 40 | name="File", default="", subtype='FILE_PATH', 41 | description = 'Destination folder.\nIf missing, the file folder will be used' 42 | ) 43 | pull : FloatProperty( 44 | name="Pull", default=5.0, min=0, soft_max=10, 45 | description='Pull material before lift' 46 | ) 47 | push : FloatProperty( 48 | name="Push", default=5.0, min=0, soft_max=10, 49 | description='Push material before start extruding' 50 | ) 51 | dz : FloatProperty( 52 | name="dz", default=2.0, min=0, soft_max=20, 53 | description='Z movement for lifting the nozzle before travel' 54 | ) 55 | flow_mult : FloatProperty( 56 | name="Flow Mult", default=1.0, min=0, soft_max=3, 57 | description = 'Flow multiplier.\nUse a single value or a list of values for changing it during the printing path' 58 | ) 59 | feed : IntProperty( 60 | name="Feed Rate (F)", default=3600, min=0, soft_max=20000, 61 | description='Printing speed' 62 | ) 63 | feed_horizontal : IntProperty( 64 | name="Feed Horizontal", default=7200, min=0, soft_max=20000, 65 | description='Travel speed' 66 | ) 67 | feed_vertical : IntProperty( 68 | name="Feed Vertical", default=3600, min=0, soft_max=20000, 69 | description='Lift movements speed' 70 | ) 71 | 72 | speed : IntProperty( 73 | name="Speed", default=60, min=0, soft_max=100, 74 | description='Printing speed' 75 | ) 76 | speed_horizontal : IntProperty( 77 | name="Travel", default=120, min=0, soft_max=200, 78 | description='Travel speed' 79 | ) 80 | speed_vertical : IntProperty( 81 | name="Z-Lift", default=60, min=0, soft_max=200, 82 | description='Lift movements speed' 83 | ) 84 | 85 | esteps : FloatProperty( 86 | name="E Steps/Unit", default=5, min=0, soft_max=100) 87 | start_code : StringProperty( 88 | name="Start", default='', description = 'Text block for starting code' 89 | ) 90 | end_code : StringProperty( 91 | name="End", default='', description = 'Text block for ending code' 92 | ) 93 | auto_sort_layers : BoolProperty( 94 | name="Auto Sort Layers", default=True, 95 | description = 'Sort layers according to the Z of the median point' 96 | ) 97 | auto_sort_points : BoolProperty( 98 | name="Auto Sort Points", default=False, 99 | description = 'Shift layer points trying to automatically reduce needed travel movements' 100 | ) 101 | close_all : BoolProperty( 102 | name="Close Shapes", default=False, 103 | description = 'Repeat the starting point at the end of the vertices list for each layer' 104 | ) 105 | nozzle : FloatProperty( 106 | name="Nozzle", default=0.4, min=0, soft_max=10, 107 | description='Nozzle diameter' 108 | ) 109 | layer_height : FloatProperty( 110 | name="Layer Height", default=0.1, min=0, soft_max=10, 111 | description = 'Average layer height, needed for a correct extrusion' 112 | ) 113 | filament : FloatProperty( 114 | name="Filament (\u03A6)", default=1.75, min=0, soft_max=120, 115 | description='Filament (or material container) diameter' 116 | ) 117 | 118 | gcode_mode : EnumProperty(items=[ 119 | ("CONT", "Continuous", ""), 120 | ("RETR", "Retraction", "") 121 | ], default='CONT', name="Mode", 122 | description = 'If retraction is used, then each separated list of vertices\nwill be considered as a different layer' 123 | ) 124 | speed_mode : EnumProperty(items=[ 125 | ("SPEED", "Speed (mm/s)", ""), 126 | ("FEED", "Feed (mm/min)", "") 127 | ], default='SPEED', name="Speed Mode", 128 | description = 'Speed control mode', 129 | update = change_speed_mode 130 | ) 131 | previous_speed_mode : StringProperty( 132 | name="previous_speed_mode", default='', description = '' 133 | ) 134 | retraction_mode : EnumProperty(items=[ 135 | ("FIRMWARE", "Firmware", ""), 136 | ("GCODE", "Gcode", "") 137 | ], default='GCODE', name="Retraction Mode", 138 | description = 'If firmware retraction is used, then the retraction parameters will be controlled by the printer' 139 | ) 140 | animate : BoolProperty( 141 | name="Animate", default=False, 142 | description = 'Show print progression according to current frame' 143 | ) 144 | use_curve_thickness : BoolProperty( 145 | name="Use Curve Thickness", default=False, 146 | description = 'Layer height depends on radius and bevel of the curve' 147 | ) 148 | 149 | 150 | class GCODE_PT_gcode_exporter(Panel): 151 | bl_category = "Gcode" 152 | bl_space_type = "VIEW_3D" 153 | bl_region_type = "UI" 154 | #bl_space_type = 'PROPERTIES' 155 | #bl_region_type = 'WINDOW' 156 | #bl_context = "data" 157 | bl_label = "Gcode Export" 158 | #bl_options = {'DEFAULT_CLOSED'} 159 | 160 | @classmethod 161 | def poll(cls, context): 162 | try: return context.object.type in ('CURVE','MESH') 163 | except: return False 164 | 165 | def draw(self, context): 166 | props = context.scene.gcode_settings 167 | 168 | #addon = context.user_preferences.addons.get(sverchok.__name__) 169 | #over_sized_buttons = addon.preferences.over_sized_buttons 170 | layout = self.layout 171 | col = layout.column(align=True) 172 | row = col.row() 173 | row.prop(props, 'folder', toggle=True, text='') 174 | col = layout.column(align=True) 175 | row = col.row() 176 | row.prop(props, 'gcode_mode', expand=True, toggle=True) 177 | #col = layout.column(align=True) 178 | col = layout.column(align=True) 179 | col.label(text="Extrusion:", icon='MOD_FLUIDSIM') 180 | #col.prop(self, 'esteps') 181 | col.prop(props, 'filament') 182 | col.prop(props, 'nozzle') 183 | if context.object.type == 'CURVE' and props.use_curve_thickness: 184 | row = col.row(align=True) 185 | row.prop(props, 'layer_height') 186 | row.enabled = False 187 | else: 188 | col.prop(props, 'layer_height') 189 | if context.object.type == 'CURVE': 190 | col.prop(props, 'use_curve_thickness') 191 | col.separator() 192 | col.label(text="Speed (Feed Rate F):", icon='DRIVER') 193 | col.prop(props, 'speed_mode', text='') 194 | speed_prefix = 'feed' if props.speed_mode == 'FEED' else 'speed' 195 | col.prop(props, speed_prefix, text='Print') 196 | if props.gcode_mode == 'RETR': 197 | col.prop(props, speed_prefix + '_vertical', text='Z Lift') 198 | col.prop(props, speed_prefix + '_horizontal', text='Travel') 199 | col.separator() 200 | if props.gcode_mode == 'RETR': 201 | col = layout.column(align=True) 202 | col.label(text="Retraction Mode:", icon='NOCURVE') 203 | row = col.row() 204 | row.prop(props, 'retraction_mode', expand=True, toggle=True) 205 | if props.retraction_mode == 'GCODE': 206 | col.separator() 207 | col.label(text="Retraction:", icon='PREFERENCES') 208 | col.prop(props, 'pull', text='Retraction') 209 | col.prop(props, 'dz', text='Z Hop') 210 | col.prop(props, 'push', text='Preload') 211 | col.separator() 212 | #col.label(text="Layers options:", icon='ALIGN_JUSTIFY') 213 | col.separator() 214 | col.prop(props, 'auto_sort_layers', text="Sort Layers (Z)") 215 | col.prop(props, 'auto_sort_points', text="Sort Points (XY)") 216 | #col.prop(props, 'close_all') 217 | col.separator() 218 | col.label(text='Custom Code:', icon='TEXT') 219 | col.prop_search(props, 'start_code', bpy.data, 'texts') 220 | col.prop_search(props, 'end_code', bpy.data, 'texts') 221 | col.separator() 222 | row = col.row(align=True) 223 | row.scale_y = 2.0 224 | row.operator('scene.gcode_export') 225 | #col.separator() 226 | #col.prop(props, 'animate', icon='TIME') 227 | 228 | 229 | class gcode_export(Operator): 230 | bl_idname = "scene.gcode_export" 231 | bl_label = "Export Gcode" 232 | bl_description = ("Export selected curve object as Gcode file") 233 | bl_options = {'REGISTER', 'UNDO'} 234 | 235 | @classmethod 236 | def poll(cls, context): 237 | try: 238 | return context.object.type in ('CURVE', 'MESH') 239 | except: 240 | return False 241 | 242 | def execute(self, context): 243 | scene = context.scene 244 | props = scene.gcode_settings 245 | # manage data 246 | if props.speed_mode == 'SPEED': 247 | props.feed = props.speed*60 248 | props.feed_vertical = props.speed_vertical*60 249 | props.feed_horizontal = props.speed_horizontal*60 250 | feed = props.feed 251 | feed_v = props.feed_vertical 252 | feed_h = props.feed_horizontal 253 | layer = props.layer_height 254 | flow_mult = props.flow_mult 255 | use_curve_thickness = props.use_curve_thickness 256 | if context.object.type != 'CURVE': use_curve_thickness = False 257 | #if context.object.type != 'CURVE': 258 | # self.report({'ERROR'}, 'Please select a Curve object') 259 | # return {'CANCELLED'} 260 | ob = context.object 261 | matr = ob.matrix_world 262 | if ob.type == 'MESH': 263 | dg = context.evaluated_depsgraph_get() 264 | mesh = ob.evaluated_get(dg).data 265 | edges = [list(e.vertices) for e in mesh.edges] 266 | verts = [v.co for v in mesh.vertices] 267 | radii = [1]*len(verts) 268 | ordered_verts = find_curves(edges, len(mesh.vertices)) 269 | ob = curve_from_pydata(verts, radii, ordered_verts, name='__temp_curve__', merge_distance=0.1, set_active=False) 270 | 271 | vertices = [[matr @ p.co.xyz for p in s.points] for s in ob.data.splines] 272 | if use_curve_thickness: 273 | bevel_depth = ob.data.bevel_depth 274 | var_height = [[p.radius * bevel_depth for p in s.points] for s in ob.data.splines] 275 | cyclic_u = [s.use_cyclic_u for s in ob.data.splines] 276 | 277 | if ob.name == '__temp_curve__': bpy.data.objects.remove(ob) 278 | 279 | if len(vertices) == 1: props.gcode_mode = 'CONT' 280 | export = True 281 | 282 | # open file 283 | if(export): 284 | if props.folder == '': 285 | folder = '//' + os.path.splitext(bpy.path.basename(bpy.context.blend_data.filepath))[0] 286 | else: 287 | folder = props.folder 288 | if '.gcode' not in folder: folder += '.gcode' 289 | path = bpy.path.abspath(folder) 290 | file = open(path, 'w') 291 | try: 292 | for line in bpy.data.texts[props.start_code].lines: 293 | file.write(line.body + '\n') 294 | except: 295 | pass 296 | 297 | #if props.gcode_mode == 'RETR': 298 | 299 | # sort layers (Z) 300 | if props.auto_sort_layers: 301 | sorted_verts = [] 302 | if use_curve_thickness: 303 | sorted_height = [] 304 | for i, curve in enumerate(vertices): 305 | # mean z 306 | listz = [v[2] for v in curve] 307 | meanz = np.mean(listz) 308 | # store curve and meanz 309 | sorted_verts.append((curve, meanz)) 310 | if use_curve_thickness: 311 | sorted_height.append((var_height[i], meanz)) 312 | vertices = [data[0] for data in sorted(sorted_verts, key=lambda height: height[1])] 313 | if use_curve_thickness: 314 | var_height = [data[0] for data in sorted(sorted_height, key=lambda height: height[1])] 315 | 316 | # sort vertices (XY) 317 | if props.auto_sort_points: 318 | # curves median point 319 | median_points = [np.mean(verts,axis=0) for verts in vertices] 320 | 321 | # chose starting point for each curve 322 | for j, curve in enumerate(vertices): 323 | # for closed curves finds the best starting point 324 | if cyclic_u[j]: 325 | # create kd tree 326 | kd = mathutils.kdtree.KDTree(len(curve)) 327 | for i, v in enumerate(curve): 328 | kd.insert(v, i) 329 | kd.balance() 330 | 331 | if props.gcode_mode == 'RETR': 332 | if j==0: 333 | # close to next two curves median point 334 | co_find = np.mean(median_points[j+1:j+3],axis=0) 335 | elif j < len(vertices)-1: 336 | co_find = np.mean([median_points[j-1],median_points[j+1]],axis=0) 337 | else: 338 | co_find = np.mean(median_points[j-2:j],axis=0) 339 | #flow_mult[j] = flow_mult[j][index:]+flow_mult[j][:index] 340 | #layer[j] = layer[j][index:]+layer[j][:index] 341 | else: 342 | if j==0: 343 | # close to next two curves median point 344 | co_find = np.mean(median_points[j+1:j+3],axis=0) 345 | else: 346 | co_find = vertices[j-1][-1] 347 | co, index, dist = kd.find(co_find) 348 | vertices[j] = vertices[j][index:]+vertices[j][:index+1] 349 | if use_curve_thickness: 350 | var_height[j] = var_height[j][index:]+var_height[j][:index+1] 351 | else: 352 | if j > 0: 353 | p0 = curve[0] 354 | p1 = curve[-1] 355 | last = vertices[j-1][-1] 356 | d0 = (last-p0).length 357 | d1 = (last-p1).length 358 | if d1 < d0: 359 | vertices[j].reverse() 360 | if use_curve_thickness: 361 | var_height[j].reverse() 362 | 363 | # calc bounding box 364 | min_corner = np.min(vertices[0],axis=0) 365 | max_corner = np.max(vertices[0],axis=0) 366 | for i in range(1,len(vertices)): 367 | eval_points = vertices[i] + [min_corner] 368 | min_corner = np.min(eval_points,axis=0) 369 | eval_points = vertices[i] + [max_corner] 370 | max_corner = np.max(eval_points,axis=0) 371 | 372 | # initialize variables 373 | e = 0 374 | last_vert = Vector((0,0,0)) 375 | maxz = 0 376 | path_length = 0 377 | travel_length = 0 378 | 379 | printed_verts = [] 380 | printed_edges = [] 381 | travel_verts = [] 382 | travel_edges = [] 383 | 384 | # write movements 385 | for i in range(len(vertices)): 386 | curve = vertices[i] 387 | first_id = len(printed_verts) 388 | for j in range(len(curve)): 389 | v = curve[j] 390 | v_flow_mult = flow_mult#[i][j] 391 | v_layer = layer#[i][j] 392 | if use_curve_thickness: 393 | v_layer = var_height[i][j]*2 394 | 395 | # record max z 396 | maxz = np.max((maxz,v[2])) 397 | #maxz = max(maxz,v[2]) 398 | 399 | # first point of the gcode 400 | if i == j == 0: 401 | printed_verts.append(v) 402 | if(export): 403 | file.write('G92 E0 \n') 404 | params = v[:3] + (feed,) 405 | to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} F{3:.0f}\n'.format(*params) 406 | file.write(to_write) 407 | else: 408 | # start after retraction 409 | if j == 0 and props.gcode_mode == 'RETR': 410 | if(export): 411 | params = v[:2] + (maxz+props.dz,) + (feed_h,) 412 | to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} F{3:.0f}\n'.format(*params) 413 | file.write(to_write) 414 | params = v[:3] + (feed_v,) 415 | to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} F{3:.0f}\n'.format(*params) 416 | file.write(to_write) 417 | to_write = 'G1 F{:.0f}\n'.format(feed) 418 | file.write(to_write) 419 | if props.retraction_mode == 'GCODE': 420 | e += props.push 421 | file.write( 'G1 E' + format(e, '.4f') + '\n') 422 | else: 423 | file.write('G11\n') 424 | printed_verts.append((v[0], v[1], maxz+props.dz)) 425 | travel_edges.append((len(printed_verts)-1, len(printed_verts)-2)) 426 | travel_length += (Vector(printed_verts[-1])-Vector(printed_verts[-2])).length 427 | printed_verts.append(v) 428 | travel_edges.append((len(printed_verts)-1, len(printed_verts)-2)) 429 | travel_length += maxz+props.dz - v[2] 430 | # regular extrusion 431 | else: 432 | printed_verts.append(v) 433 | v1 = Vector(v) 434 | v0 = Vector(curve[j-1]) 435 | dist = (v1-v0).length 436 | area = v_layer * props.nozzle + pi*(v_layer/2)**2 # rectangle + circle 437 | cylinder = pi*(props.filament/2)**2 438 | flow = area / cylinder * (0 if j == 0 else 1) 439 | e += dist * v_flow_mult * flow 440 | params = v[:3] + (e,) 441 | if(export): 442 | to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} E{3:.4f}\n'.format(*params) 443 | file.write(to_write) 444 | path_length += dist 445 | printed_edges.append([len(printed_verts)-1, len(printed_verts)-2]) 446 | if props.gcode_mode == 'RETR': 447 | v0 = Vector(curve[-1]) 448 | if props.close_all and False: 449 | #printed_verts.append(v0) 450 | printed_edges.append([len(printed_verts)-1, first_id]) 451 | 452 | v1 = Vector(curve[0]) 453 | dist = (v0-v1).length 454 | area = v_layer * props.nozzle + pi*(v_layer/2)**2 # rectangle + circle 455 | cylinder = pi*(props.filament/2)**2 456 | flow = area / cylinder 457 | e += dist * v_flow_mult * flow 458 | params = v1[:3] + (e,) 459 | if(export): 460 | to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} E{3:.4f}\n'.format(*params) 461 | file.write(to_write) 462 | path_length += dist 463 | v0 = v1 464 | if i < len(vertices)-1: 465 | if(export): 466 | if props.retraction_mode == 'GCODE': 467 | e -= props.pull 468 | file.write('G0 E' + format(e, '.4f') + '\n') 469 | else: 470 | file.write('G10\n') 471 | params = v0[:2] + (maxz+props.dz,) + (feed_v,) 472 | to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} F{3:.0f}\n'.format(*params) 473 | file.write(to_write) 474 | printed_verts.append(v0.to_tuple()) 475 | printed_verts.append((v0.x, v0.y, maxz+props.dz)) 476 | travel_edges.append((len(printed_verts)-1, len(printed_verts)-2)) 477 | travel_length += maxz+props.dz - v0.z 478 | if(export): 479 | # end code 480 | try: 481 | for line in bpy.data.texts[props.end_code].lines: 482 | file.write(line.body + '\n') 483 | except: 484 | pass 485 | file.close() 486 | print("Saved gcode to " + path) 487 | bb = list(min_corner) + list(max_corner) 488 | info = 'Bounding Box:\n' 489 | info += '\tmin\tX: {0:.1f}\tY: {1:.1f}\tZ: {2:.1f}\n'.format(*bb) 490 | info += '\tmax\tX: {3:.1f}\tY: {4:.1f}\tZ: {5:.1f}\n'.format(*bb) 491 | info += 'Extruded Filament: ' + format(e, '.2f') + '\n' 492 | info += 'Extruded Volume: ' + format(e*pi*(props.filament/2)**2, '.2f') + '\n' 493 | info += 'Printed Path Length: ' + format(path_length, '.2f') + '\n' 494 | info += 'Travel Length: ' + format(travel_length, '.2f') 495 | ''' 496 | # animate 497 | if scene.animate: 498 | scene = bpy.context.scene 499 | try: 500 | param = (scene.frame_current - scene.frame_start)/(scene.frame_end - scene.frame_start) 501 | except: 502 | param = 1 503 | last_vert = max(int(param*len(printed_verts)),1) 504 | printed_verts = printed_verts[:last_vert] 505 | printed_edges = [e for e in printed_edges if e[0] < last_vert and e[1] < last_vert] 506 | travel_edges = [e for e in travel_edges if e[0] < last_vert and e[1] < last_vert] 507 | ''' 508 | return {'FINISHED'} 509 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | import bpy 20 | import threading 21 | import numpy as np 22 | import multiprocessing 23 | from multiprocessing import Process, Pool 24 | from mathutils import Vector 25 | try: from .numba_functions import numba_lerp2 26 | except: pass 27 | 28 | weight = [] 29 | n_threads = multiprocessing.cpu_count() 30 | 31 | def find_curves(edges, n_verts): 32 | verts_dict = {key:[] for key in range(n_verts)} 33 | for e in edges: 34 | verts_dict[e[0]].append(e[1]) 35 | verts_dict[e[1]].append(e[0]) 36 | curves = [] 37 | while True: 38 | if len(verts_dict) == 0: break 39 | # next starting point 40 | v = list(verts_dict.keys())[0] 41 | # neighbors 42 | v01 = verts_dict[v] 43 | if len(v01) == 0: 44 | verts_dict.pop(v) 45 | continue 46 | curve = [] 47 | if len(v01) > 1: curve.append(v01[1]) # add neighbors 48 | curve.append(v) # add starting point 49 | curve.append(v01[0]) # add neighbors 50 | verts_dict.pop(v) 51 | # start building curve 52 | while True: 53 | #last_point = curve[-1] 54 | #if last_point not in verts_dict: break 55 | 56 | # try to change direction if needed 57 | if curve[-1] in verts_dict: pass 58 | elif curve[0] in verts_dict: curve.reverse() 59 | else: break 60 | 61 | # neighbors points 62 | last_point = curve[-1] 63 | v01 = verts_dict[last_point] 64 | 65 | # curve end 66 | if len(v01) == 1: 67 | verts_dict.pop(last_point) 68 | if curve[0] in verts_dict: continue 69 | else: break 70 | 71 | # chose next point 72 | new_point = None 73 | if v01[0] == curve[-2]: new_point = v01[1] 74 | elif v01[1] == curve[-2]: new_point = v01[0] 75 | #else: break 76 | 77 | #if new_point != curve[1]: 78 | curve.append(new_point) 79 | verts_dict.pop(last_point) 80 | if curve[0] == curve[-1]: 81 | verts_dict.pop(new_point) 82 | break 83 | curves.append(curve) 84 | return curves 85 | 86 | def curve_from_points(points, name='Curve'): 87 | curve = bpy.data.curves.new(name,'CURVE') 88 | for c in points: 89 | s = curve.splines.new('POLY') 90 | s.points.add(len(c)) 91 | for i,p in enumerate(c): s.points[i].co = p.xyz + [1] 92 | ob_curve = bpy.data.objects.new(name,curve) 93 | return ob_curve 94 | 95 | def curve_from_pydata(points, radii, indexes, name='Curve', skip_open=False, merge_distance=1, set_active=True): 96 | curve = bpy.data.curves.new(name,'CURVE') 97 | curve.dimensions = '3D' 98 | for c in indexes: 99 | # cleanup 100 | pts = np.array([points[i] for i in c]) 101 | rad = np.array([radii[i] for i in c]) 102 | if merge_distance > 0: 103 | pts1 = np.roll(pts,1,axis=0) 104 | dist = np.linalg.norm(pts1-pts, axis=1) 105 | count = 0 106 | n = len(dist) 107 | mask = np.ones(n).astype('bool') 108 | for i in range(n): 109 | count += dist[i] 110 | if count > merge_distance: count = 0 111 | else: mask[i] = False 112 | pts = pts[mask] 113 | rad = rad[mask] 114 | 115 | bool_cyclic = c[0] == c[-1] 116 | if skip_open and not bool_cyclic: continue 117 | s = curve.splines.new('POLY') 118 | n_pts = len(pts) 119 | s.points.add(n_pts-1) 120 | w = np.ones(n_pts).reshape((n_pts,1)) 121 | co = np.concatenate((pts,w),axis=1).reshape((n_pts*4)) 122 | s.points.foreach_set('co',co) 123 | s.points.foreach_set('radius',rad) 124 | s.use_cyclic_u = bool_cyclic 125 | ob_curve = bpy.data.objects.new(name,curve) 126 | bpy.context.collection.objects.link(ob_curve) 127 | if set_active: 128 | bpy.context.view_layer.objects.active = ob_curve 129 | return ob_curve 130 | 131 | def curve_from_vertices(indexes, verts, name='Curve'): 132 | curve = bpy.data.curves.new(name,'CURVE') 133 | for c in indexes: 134 | s = curve.splines.new('POLY') 135 | s.points.add(len(c)) 136 | for i,p in enumerate(c): s.points[i].co = verts[p].co.xyz + [1] 137 | ob_curve = bpy.data.objects.new(name,curve) 138 | return ob_curve 139 | --------------------------------------------------------------------------------