2019-11-15 18:13:09 +00:00
|
|
|
# blender CAM simple.py (c) 2012 Vilem Novak
|
|
|
|
#
|
|
|
|
# ***** 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 LICENCE BLOCK *****
|
|
|
|
|
2023-07-21 10:49:06 +00:00
|
|
|
# Solves: No module named 'shapely' (even if it is installed)
|
2023-11-30 19:01:04 +00:00
|
|
|
#help('modules')
|
2023-07-21 10:49:06 +00:00
|
|
|
|
2022-01-21 17:24:35 +00:00
|
|
|
import math
|
|
|
|
import sys
|
|
|
|
import os
|
|
|
|
import string
|
2019-11-15 18:13:09 +00:00
|
|
|
import time
|
|
|
|
import bpy
|
|
|
|
import mathutils
|
|
|
|
from mathutils import *
|
|
|
|
from math import *
|
2022-01-16 14:47:08 +00:00
|
|
|
from shapely.geometry import Point, LineString, Polygon, MultiLineString
|
2019-11-15 18:13:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
def tuple_add(t, t1): # add two tuples as Vectors
|
2022-01-16 15:16:29 +00:00
|
|
|
return t[0] + t1[0], t[1] + t1[1], t[2] + t1[2]
|
2019-11-15 18:13:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
def tuple_sub(t, t1): # sub two tuples as Vectors
|
2022-01-16 15:16:29 +00:00
|
|
|
return t[0] - t1[0], t[1] - t1[1], t[2] - t1[2]
|
2019-11-15 18:13:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
def tuple_mul(t, c): # multiply two tuples with a number
|
2022-01-16 15:16:29 +00:00
|
|
|
return t[0] * c, t[1] * c, t[2] * c
|
2019-11-15 18:13:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
def tuple_length(t): # get length of vector, but passed in as tuple.
|
2022-01-16 15:16:29 +00:00
|
|
|
return Vector(t).length
|
2019-11-15 18:13:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
# timing functions for optimisation purposes...
|
|
|
|
def timinginit():
|
|
|
|
return [0, 0]
|
|
|
|
|
|
|
|
|
|
|
|
def timingstart(tinf):
|
|
|
|
t = time.time()
|
|
|
|
tinf[1] = t
|
|
|
|
|
|
|
|
|
|
|
|
def timingadd(tinf):
|
|
|
|
t = time.time()
|
|
|
|
tinf[0] += t - tinf[1]
|
|
|
|
|
|
|
|
|
|
|
|
def timingprint(tinf):
|
|
|
|
print('time ' + str(tinf[0]) + 'seconds')
|
|
|
|
|
|
|
|
|
|
|
|
def progress(text, n=None):
|
2020-10-12 16:45:08 +00:00
|
|
|
"""function for reporting during the script, works for background operations in the header."""
|
2019-11-15 18:13:09 +00:00
|
|
|
text = str(text)
|
2022-01-16 15:04:40 +00:00
|
|
|
if n is None:
|
2019-11-15 18:13:09 +00:00
|
|
|
n = ''
|
|
|
|
else:
|
|
|
|
n = ' ' + str(int(n * 1000) / 1000) + '%'
|
|
|
|
sys.stdout.write('progress{%s%s}\n' % (text, n))
|
|
|
|
sys.stdout.flush()
|
|
|
|
|
|
|
|
|
|
|
|
def activate(o):
|
2020-10-12 16:45:08 +00:00
|
|
|
"""makes an object active, used many times in blender"""
|
2019-11-15 18:13:09 +00:00
|
|
|
s = bpy.context.scene
|
|
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
|
|
o.select_set(state=True)
|
|
|
|
s.objects[o.name].select_set(state=True)
|
|
|
|
bpy.context.view_layer.objects.active = o
|
|
|
|
|
|
|
|
|
|
|
|
def dist2d(v1, v2):
|
2020-10-12 16:45:08 +00:00
|
|
|
"""distance between two points in 2d"""
|
2019-11-15 18:13:09 +00:00
|
|
|
return math.hypot((v1[0] - v2[0]), (v1[1] - v2[1]))
|
|
|
|
|
|
|
|
|
|
|
|
def delob(ob):
|
2020-10-12 16:45:08 +00:00
|
|
|
"""object deletion for multiple uses"""
|
2019-11-15 18:13:09 +00:00
|
|
|
activate(ob)
|
|
|
|
bpy.ops.object.delete(use_global=False)
|
|
|
|
|
|
|
|
|
|
|
|
def dupliob(o, pos):
|
2020-10-12 16:45:08 +00:00
|
|
|
"""helper function for visualising cutter positions in bullet simulation"""
|
2019-11-15 18:13:09 +00:00
|
|
|
activate(o)
|
|
|
|
bpy.ops.object.duplicate()
|
|
|
|
s = 1.0 / BULLET_SCALE
|
|
|
|
bpy.ops.transform.resize(value=(s, s, s), constraint_axis=(False, False, False), orient_type='GLOBAL',
|
|
|
|
mirror=False, use_proportional_edit=False, proportional_edit_falloff='SMOOTH',
|
|
|
|
proportional_size=1)
|
|
|
|
o = bpy.context.active_object
|
|
|
|
bpy.ops.rigidbody.object_remove()
|
|
|
|
o.location = pos
|
|
|
|
|
|
|
|
|
|
|
|
def addToGroup(ob, groupname):
|
|
|
|
activate(ob)
|
2022-01-16 15:04:40 +00:00
|
|
|
if bpy.data.groups.get(groupname) is None:
|
2019-11-15 18:13:09 +00:00
|
|
|
bpy.ops.group.create(name=groupname)
|
|
|
|
else:
|
|
|
|
bpy.ops.object.group_link(group=groupname)
|
|
|
|
|
|
|
|
|
|
|
|
def compare(v1, v2, vmiddle, e):
|
2020-10-12 16:45:08 +00:00
|
|
|
"""comparison for optimisation of paths"""
|
2019-11-15 18:13:09 +00:00
|
|
|
# e=0.0001
|
|
|
|
v1 = Vector(v1)
|
|
|
|
v2 = Vector(v2)
|
|
|
|
vmiddle = Vector(vmiddle)
|
|
|
|
vect1 = v2 - v1
|
|
|
|
vect2 = vmiddle - v1
|
|
|
|
vect1.normalize()
|
|
|
|
vect1 *= vect2.length
|
|
|
|
v = vect2 - vect1
|
|
|
|
if v.length < e:
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def isVerticalLimit(v1, v2, limit):
|
2020-10-12 16:45:08 +00:00
|
|
|
"""test path segment on verticality threshold, for protect_vertical option"""
|
2019-11-15 18:13:09 +00:00
|
|
|
z = abs(v1[2] - v2[2])
|
|
|
|
# verticality=0.05
|
|
|
|
# this will be better.
|
|
|
|
#
|
|
|
|
# print(a)
|
|
|
|
if z > 0:
|
|
|
|
v2d = Vector((0, 0, -1))
|
|
|
|
v3d = Vector((v1[0] - v2[0], v1[1] - v2[1], v1[2] - v2[2]))
|
|
|
|
a = v3d.angle(v2d)
|
|
|
|
if a > pi / 2:
|
|
|
|
a = abs(a - pi)
|
|
|
|
# print(a)
|
|
|
|
if a < limit:
|
|
|
|
# print(abs(v1[0]-v2[0])/z)
|
|
|
|
# print(abs(v1[1]-v2[1])/z)
|
|
|
|
if v1[2] > v2[2]:
|
|
|
|
v1 = (v2[0], v2[1], v1[2])
|
|
|
|
return v1, v2
|
|
|
|
else:
|
|
|
|
v2 = (v1[0], v1[1], v2[2])
|
|
|
|
return v1, v2
|
|
|
|
return v1, v2
|
|
|
|
|
|
|
|
|
|
|
|
def getCachePath(o):
|
|
|
|
fn = bpy.data.filepath
|
|
|
|
l = len(bpy.path.basename(fn))
|
|
|
|
bn = bpy.path.basename(fn)[:-6]
|
2022-01-16 15:16:29 +00:00
|
|
|
print('fn-l:', fn[:-l])
|
|
|
|
print('bn:', bn)
|
2019-11-15 18:13:09 +00:00
|
|
|
|
|
|
|
iname = fn[:-l] + 'temp_cam' + os.sep + bn + '_' + o.name
|
|
|
|
return iname
|
|
|
|
|
2022-01-16 15:16:29 +00:00
|
|
|
|
2021-08-12 15:50:34 +00:00
|
|
|
def getSimulationPath():
|
|
|
|
fn = bpy.data.filepath
|
|
|
|
l = len(bpy.path.basename(fn))
|
|
|
|
iname = fn[:-l] + 'temp_cam' + os.sep
|
|
|
|
return iname
|
2019-11-15 18:13:09 +00:00
|
|
|
|
2022-01-16 15:04:40 +00:00
|
|
|
|
2019-11-15 18:13:09 +00:00
|
|
|
def safeFileName(name): # for export gcode
|
|
|
|
valid_chars = "-_.()%s%s" % (string.ascii_letters, string.digits)
|
|
|
|
filename = ''.join(c for c in name if c in valid_chars)
|
|
|
|
return filename
|
|
|
|
|
|
|
|
|
|
|
|
def strInUnits(x, precision=5):
|
|
|
|
if bpy.context.scene.unit_settings.system == 'METRIC':
|
|
|
|
return str(round(x * 1000, precision)) + ' mm '
|
|
|
|
elif bpy.context.scene.unit_settings.system == 'IMPERIAL':
|
|
|
|
return str(round(x * 1000 / 25.4, precision)) + "'' "
|
|
|
|
else:
|
|
|
|
return str(x)
|
2021-08-30 17:49:06 +00:00
|
|
|
|
2022-01-16 15:04:40 +00:00
|
|
|
|
2022-01-22 21:41:02 +00:00
|
|
|
# select multiple object starting with name
|
|
|
|
def select_multiple(name):
|
2021-08-30 17:49:06 +00:00
|
|
|
scene = bpy.context.scene
|
2022-01-22 21:41:02 +00:00
|
|
|
bpy.ops.object.select_all(action='DESELECT')
|
2021-08-30 17:49:06 +00:00
|
|
|
for ob in scene.objects: # join pocket curve calculations
|
|
|
|
if ob.name.startswith(name):
|
|
|
|
ob.select_set(True)
|
|
|
|
else:
|
|
|
|
ob.select_set(False)
|
|
|
|
|
2022-01-16 15:04:40 +00:00
|
|
|
|
2022-01-22 21:41:02 +00:00
|
|
|
# join multiple objects starting with 'name' renaming final object as 'name'
|
|
|
|
def join_multiple(name):
|
|
|
|
select_multiple(name)
|
|
|
|
bpy.ops.object.join()
|
|
|
|
bpy.context.active_object.name = name # rename object
|
2021-09-08 13:40:47 +00:00
|
|
|
|
2022-01-16 15:04:40 +00:00
|
|
|
|
2021-08-31 18:10:45 +00:00
|
|
|
# remove multiple objects starting with 'name'.... useful for fixed name operation
|
2022-01-22 21:41:02 +00:00
|
|
|
def remove_multiple(name):
|
2021-08-30 17:49:06 +00:00
|
|
|
scene = bpy.context.scene
|
|
|
|
bpy.ops.object.select_all(action='DESELECT')
|
2021-08-31 18:10:45 +00:00
|
|
|
for ob in scene.objects:
|
2021-08-30 17:49:06 +00:00
|
|
|
if ob.name.startswith(name):
|
|
|
|
ob.select_set(True)
|
|
|
|
bpy.ops.object.delete()
|
2021-12-05 20:03:11 +00:00
|
|
|
|
2021-12-29 20:13:02 +00:00
|
|
|
|
2022-01-29 15:51:36 +00:00
|
|
|
def deselect():
|
|
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
|
|
|
|
|
|
|
2022-01-16 15:04:40 +00:00
|
|
|
# makes the object with the name active
|
2022-01-22 21:41:02 +00:00
|
|
|
def make_active(name):
|
2021-12-05 20:03:11 +00:00
|
|
|
ob = bpy.context.scene.objects[name]
|
|
|
|
bpy.ops.object.select_all(action='DESELECT')
|
2022-01-16 15:04:40 +00:00
|
|
|
bpy.context.view_layer.objects.active = ob
|
2021-12-05 20:03:11 +00:00
|
|
|
ob.select_set(True)
|
2021-12-29 20:13:02 +00:00
|
|
|
|
|
|
|
|
2022-01-16 15:04:40 +00:00
|
|
|
# change the name of the active object
|
2022-01-21 17:15:05 +00:00
|
|
|
def active_name(name):
|
2021-12-30 21:13:53 +00:00
|
|
|
bpy.context.active_object.name = name
|
2021-12-29 20:13:02 +00:00
|
|
|
|
2022-01-02 16:28:39 +00:00
|
|
|
|
2022-01-16 15:04:40 +00:00
|
|
|
# renames and makes active name and makes it active
|
|
|
|
def rename(name, name2):
|
2022-01-22 21:41:02 +00:00
|
|
|
make_active(name)
|
2022-01-02 16:28:39 +00:00
|
|
|
bpy.context.active_object.name = name2
|
|
|
|
|
|
|
|
|
2022-01-16 15:04:40 +00:00
|
|
|
# boolean union of objects starting with name result is object name.
|
|
|
|
# all objects starting with name will be deleted and the result will be name
|
2022-01-02 16:28:39 +00:00
|
|
|
def union(name):
|
2022-01-22 21:41:02 +00:00
|
|
|
select_multiple(name)
|
2022-01-02 16:28:39 +00:00
|
|
|
bpy.ops.object.curve_boolean(boolean_type='UNION')
|
2022-01-21 17:15:05 +00:00
|
|
|
active_name('unionboolean')
|
2022-01-22 21:41:02 +00:00
|
|
|
remove_multiple(name)
|
2022-01-06 14:17:21 +00:00
|
|
|
rename('unionboolean', name)
|
2022-01-04 16:28:31 +00:00
|
|
|
|
2022-02-13 12:38:21 +00:00
|
|
|
def intersect(name):
|
|
|
|
select_multiple(name)
|
|
|
|
bpy.ops.object.curve_boolean(boolean_type='INTERSECT')
|
|
|
|
active_name('intersection')
|
2022-01-02 16:28:39 +00:00
|
|
|
|
2022-01-16 15:04:40 +00:00
|
|
|
# boolean difference of objects starting with name result is object from basename.
|
|
|
|
# all objects starting with name will be deleted and the result will be basename
|
2022-01-06 14:17:21 +00:00
|
|
|
def difference(name, basename):
|
|
|
|
# name is the series to select
|
|
|
|
# basename is what the base you want to cut including name
|
2022-01-22 21:41:02 +00:00
|
|
|
select_multiple(name)
|
2022-01-06 14:17:21 +00:00
|
|
|
bpy.context.view_layer.objects.active = bpy.data.objects[basename]
|
2022-01-02 16:28:39 +00:00
|
|
|
bpy.ops.object.curve_boolean(boolean_type='DIFFERENCE')
|
2022-01-21 17:15:05 +00:00
|
|
|
active_name('booleandifference')
|
2022-01-22 21:41:02 +00:00
|
|
|
remove_multiple(name)
|
2022-01-06 14:17:21 +00:00
|
|
|
rename('booleandifference', basename)
|
2022-01-03 19:43:58 +00:00
|
|
|
|
2022-01-16 15:04:40 +00:00
|
|
|
|
|
|
|
# duplicate active object or duplicate move
|
|
|
|
# if x or y not the default, duplicate move will be executed
|
2022-01-04 20:46:22 +00:00
|
|
|
def duplicate(x=0, y=0):
|
|
|
|
if x == 0 and y == 0:
|
2022-01-07 13:43:45 +00:00
|
|
|
bpy.ops.object.duplicate()
|
|
|
|
else:
|
2022-01-04 20:46:22 +00:00
|
|
|
bpy.ops.object.duplicate_move(OBJECT_OT_duplicate={"linked": False, "mode": 'TRANSLATION'},
|
|
|
|
TRANSFORM_OT_translate={"value": (x, y, 0.0)})
|
|
|
|
|
|
|
|
|
2022-01-16 15:04:40 +00:00
|
|
|
# Mirror active object along the x axis
|
2022-01-04 20:46:22 +00:00
|
|
|
def mirrorx():
|
|
|
|
bpy.ops.transform.mirror(orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)),
|
|
|
|
orient_matrix_type='GLOBAL', constraint_axis=(True, False, False))
|
|
|
|
|
|
|
|
|
2022-01-16 15:04:40 +00:00
|
|
|
# mirror active object along y axis
|
2022-01-04 20:46:22 +00:00
|
|
|
def mirrory():
|
|
|
|
bpy.ops.transform.mirror(orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)),
|
|
|
|
orient_matrix_type='GLOBAL', constraint_axis=(False, True, False))
|
|
|
|
|
|
|
|
|
2022-01-16 15:04:40 +00:00
|
|
|
# move active object and apply translation
|
2022-01-04 20:46:22 +00:00
|
|
|
def move(x=0.0, y=0.0):
|
|
|
|
bpy.ops.transform.translate(value=(x, y, 0.0))
|
|
|
|
bpy.ops.object.transform_apply(location=True)
|
|
|
|
|
|
|
|
|
2022-01-16 15:04:40 +00:00
|
|
|
# Rotate active object and apply rotation
|
2022-01-04 20:46:22 +00:00
|
|
|
def rotate(angle):
|
|
|
|
bpy.context.object.rotation_euler[2] = angle
|
|
|
|
bpy.ops.object.transform_apply(rotation=True)
|
|
|
|
|
|
|
|
|
2022-01-16 15:04:40 +00:00
|
|
|
# remove doubles
|
2022-01-21 17:28:28 +00:00
|
|
|
def remove_doubles():
|
2022-01-04 20:46:22 +00:00
|
|
|
bpy.ops.object.curve_remove_doubles()
|
|
|
|
|
2022-01-16 15:04:40 +00:00
|
|
|
|
|
|
|
# Add overcut to active object
|
2022-01-19 14:23:25 +00:00
|
|
|
def add_overcut(diametre, overcut=True):
|
2022-01-09 21:25:13 +00:00
|
|
|
if overcut:
|
|
|
|
name = bpy.context.active_object.name
|
|
|
|
bpy.ops.object.curve_overcuts(diameter=diametre, threshold=math.pi/2.05)
|
|
|
|
overcut_name = bpy.context.active_object.name
|
2022-01-22 21:41:02 +00:00
|
|
|
make_active(name)
|
2022-01-09 21:25:13 +00:00
|
|
|
bpy.ops.object.delete()
|
|
|
|
rename(overcut_name, name)
|
2022-01-21 17:28:28 +00:00
|
|
|
remove_doubles()
|
2022-01-04 20:46:22 +00:00
|
|
|
|
|
|
|
|
2022-01-16 15:04:40 +00:00
|
|
|
# add bounding rectangtle to curve
|
2022-01-21 13:13:58 +00:00
|
|
|
def add_bound_rectangle(xmin, ymin, xmax, ymax, name='bounds_rectangle'):
|
2022-01-15 15:45:55 +00:00
|
|
|
# xmin = minimum corner x value
|
|
|
|
# ymin = minimum corner y value
|
|
|
|
# xmax = maximum corner x value
|
|
|
|
# ymax = maximum corner y value
|
|
|
|
# name = name of the resulting object
|
|
|
|
xsize = xmax - xmin
|
|
|
|
ysize = ymax - ymin
|
|
|
|
|
2022-01-16 15:04:40 +00:00
|
|
|
bpy.ops.curve.simple(align='WORLD', location=(xmin + xsize/2, ymin + ysize/2, 0), rotation=(0, 0, 0),
|
|
|
|
Simple_Type='Rectangle',
|
2022-01-15 15:45:55 +00:00
|
|
|
Simple_width=xsize, Simple_length=ysize, use_cyclic_u=True, edit_mode=False, shape='3D')
|
2022-01-17 13:27:08 +00:00
|
|
|
bpy.ops.object.transform_apply(location=True)
|
2022-01-21 17:15:05 +00:00
|
|
|
active_name(name)
|
2022-01-15 15:45:55 +00:00
|
|
|
|
|
|
|
|
2022-01-21 13:13:58 +00:00
|
|
|
def add_rectangle(width, height, center_x=True, center_y=True):
|
|
|
|
x_offset = width / 2
|
|
|
|
y_offset = height / 2
|
|
|
|
|
|
|
|
if center_x:
|
|
|
|
x_offset = 0
|
|
|
|
if center_y:
|
|
|
|
y_offset = 0
|
|
|
|
|
|
|
|
bpy.ops.curve.simple(align='WORLD', location=(x_offset, y_offset, 0), rotation=(0, 0, 0),
|
|
|
|
Simple_Type='Rectangle',
|
|
|
|
Simple_width=width, Simple_length=height, use_cyclic_u=True, edit_mode=False, shape='3D')
|
|
|
|
bpy.ops.object.transform_apply(location=True)
|
2022-01-21 17:15:05 +00:00
|
|
|
active_name('simple_rectangle')
|
2022-01-21 13:13:58 +00:00
|
|
|
|
|
|
|
|
2022-01-16 15:04:40 +00:00
|
|
|
# Returns coords from active object
|
2022-01-21 17:24:35 +00:00
|
|
|
def active_to_coords():
|
2022-01-16 14:47:08 +00:00
|
|
|
bpy.ops.object.duplicate()
|
|
|
|
obj = bpy.context.active_object
|
|
|
|
bpy.ops.object.convert(target='MESH')
|
2022-01-21 17:15:05 +00:00
|
|
|
active_name("_tmp_mesh")
|
2022-01-16 14:47:08 +00:00
|
|
|
|
|
|
|
coords = []
|
|
|
|
for v in obj.data.vertices: # extract X,Y coordinates from the vertices data
|
|
|
|
coords.append((v.co.x, v.co.y))
|
2022-01-22 21:41:02 +00:00
|
|
|
remove_multiple('_tmp_mesh')
|
2022-01-16 14:47:08 +00:00
|
|
|
return coords
|
|
|
|
|
|
|
|
|
2022-01-16 15:04:40 +00:00
|
|
|
# returns shapely polygon from active object
|
2022-01-21 17:24:35 +00:00
|
|
|
def active_to_shapely_poly():
|
|
|
|
return Polygon(active_to_coords()) # convert coordinates to shapely Polygon datastructure
|