kopia lustrzana https://github.com/vilemduha/blendercam
1729 wiersze
64 KiB
Python
1729 wiersze
64 KiB
Python
# blender CAM utils.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 *****
|
|
|
|
# here is the main functionality of Blender CAM. The functions here are called with operators defined in ops.py.
|
|
|
|
import bpy
|
|
import time
|
|
import mathutils
|
|
import math
|
|
from math import *
|
|
from mathutils import *
|
|
from bpy.props import *
|
|
import bl_operators
|
|
from bpy.types import Menu, Operator
|
|
from bpy_extras import object_utils
|
|
import curve_simplify
|
|
import bmesh
|
|
|
|
import numpy
|
|
import random, sys, os
|
|
import pickle
|
|
import string
|
|
from cam import chunk
|
|
from cam.chunk import *
|
|
from cam import collision
|
|
from cam.collision import *
|
|
# import multiprocessing
|
|
from cam import simple
|
|
from cam.simple import *
|
|
from cam import pattern
|
|
from cam.pattern import *
|
|
from cam import polygon_utils_cam
|
|
from cam.polygon_utils_cam import *
|
|
from cam import image_utils
|
|
from cam.image_utils import *
|
|
from cam.nc import nc
|
|
from cam.nc import iso
|
|
from cam.opencamlib.opencamlib import oclSample, oclSamplePoints, oclResampleChunks, oclGetWaterline
|
|
|
|
from shapely.geometry import polygon as spolygon
|
|
from shapely import ops as sops
|
|
from shapely import geometry as sgeometry
|
|
from shapely import affinity, prepared
|
|
|
|
# from shapely.geometry import * not possible until Polygon libs gets out finally..
|
|
SHAPELY = True
|
|
|
|
|
|
def positionObject(operation):
|
|
ob = bpy.data.objects[operation.object_name]
|
|
bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS')
|
|
ob.select_set(True)
|
|
bpy.context.view_layer.objects.active = ob
|
|
|
|
minx, miny, minz, maxx, maxy, maxz = getBoundsWorldspace([ob], operation.use_modifiers)
|
|
totx=maxx-minx
|
|
toty=maxy-miny
|
|
totz=maxz-minz
|
|
if operation.material_center_x:
|
|
ob.location.x -= minx +totx/2
|
|
else:
|
|
ob.location.x -= minx
|
|
|
|
if operation.material_center_y:
|
|
ob.location.y -= miny +toty/2
|
|
else:
|
|
ob.location.y -= miny
|
|
|
|
if operation.material_Z== 'BELOW':
|
|
ob.location.z -= maxz
|
|
elif operation.material_Z == 'ABOVE':
|
|
ob.location.z -= minz
|
|
elif operation.material_Z == 'CENTERED':
|
|
ob.location.z -= minz +totz/2
|
|
|
|
if ob.type != 'CURVE':
|
|
bpy.ops.object.transform_apply(location=True, rotation=False, scale=False)
|
|
#addMaterialAreaObject()
|
|
|
|
|
|
def getBoundsWorldspace(obs, use_modifiers=False):
|
|
# progress('getting bounds of object(s)')
|
|
t = time.time()
|
|
|
|
maxx = maxy = maxz = -10000000
|
|
minx = miny = minz = 10000000
|
|
for ob in obs:
|
|
# bb=ob.bound_box
|
|
mw = ob.matrix_world
|
|
if ob.type == 'MESH':
|
|
if use_modifiers:
|
|
depsgraph = bpy.context.evaluated_depsgraph_get()
|
|
mesh_owner = ob.evaluated_get(depsgraph)
|
|
mesh = mesh_owner.to_mesh()
|
|
else:
|
|
mesh = ob.data
|
|
|
|
for c in mesh.vertices:
|
|
coord = c.co
|
|
worldCoord = mw @ Vector((coord[0], coord[1], coord[2]))
|
|
minx = min(minx, worldCoord.x)
|
|
miny = min(miny, worldCoord.y)
|
|
minz = min(minz, worldCoord.z)
|
|
maxx = max(maxx, worldCoord.x)
|
|
maxy = max(maxy, worldCoord.y)
|
|
maxz = max(maxz, worldCoord.z)
|
|
|
|
if use_modifiers:
|
|
mesh_owner.to_mesh_clear()
|
|
|
|
elif ob.type == "FONT":
|
|
activate(ob)
|
|
bpy.ops.object.duplicate()
|
|
|
|
bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM')
|
|
|
|
co = bpy.context.active_object
|
|
bpy.ops.object.convert(target='MESH', keep_original=False)
|
|
|
|
if use_modifiers:
|
|
mesh = co.to_mesh(preserve_all_data_layers=True, depsgraph=bpy.context.evaluated_depsgraph_get())
|
|
else:
|
|
mesh = co.data
|
|
|
|
for c in mesh.vertices:
|
|
coord = c.co
|
|
worldCoord = mw @ Vector((coord[0], coord[1], coord[2]))
|
|
minx = min(minx, worldCoord.x)
|
|
miny = min(miny, worldCoord.y)
|
|
minz = min(minz, worldCoord.z)
|
|
maxx = max(maxx, worldCoord.x)
|
|
maxy = max(maxy, worldCoord.y)
|
|
maxz = max(maxz, worldCoord.z)
|
|
|
|
if use_modifiers:
|
|
bpy.data.meshes.remove(mesh)
|
|
bpy.ops.object.delete()
|
|
else:
|
|
|
|
# for coord in bb:
|
|
for c in ob.data.splines:
|
|
for p in c.bezier_points:
|
|
coord = p.co
|
|
# this can work badly with some imported curves, don't know why...
|
|
# worldCoord = mw * Vector((coord[0]/ob.scale.x, coord[1]/ob.scale.y, coord[2]/ob.scale.z))
|
|
worldCoord = mw @ Vector((coord[0], coord[1], coord[2]))
|
|
minx = min(minx, worldCoord.x)
|
|
miny = min(miny, worldCoord.y)
|
|
minz = min(minz, worldCoord.z)
|
|
maxx = max(maxx, worldCoord.x)
|
|
maxy = max(maxy, worldCoord.y)
|
|
maxz = max(maxz, worldCoord.z)
|
|
for p in c.points:
|
|
coord = p.co
|
|
# this can work badly with some imported curves, don't know why...
|
|
# worldCoord = mw * Vector((coord[0]/ob.scale.x, coord[1]/ob.scale.y, coord[2]/ob.scale.z))
|
|
worldCoord = mw @ Vector((coord[0], coord[1], coord[2]))
|
|
minx = min(minx, worldCoord.x)
|
|
miny = min(miny, worldCoord.y)
|
|
minz = min(minz, worldCoord.z)
|
|
maxx = max(maxx, worldCoord.x)
|
|
maxy = max(maxy, worldCoord.y)
|
|
maxz = max(maxz, worldCoord.z)
|
|
# progress(time.time()-t)
|
|
return minx, miny, minz, maxx, maxy, maxz
|
|
|
|
|
|
def getSplineBounds(ob, curve):
|
|
# progress('getting bounds of object(s)')
|
|
maxx = maxy = maxz = -10000000
|
|
minx = miny = minz = 10000000
|
|
mw = ob.matrix_world
|
|
|
|
for p in curve.bezier_points:
|
|
coord = p.co
|
|
# this can work badly with some imported curves, don't know why...
|
|
# worldCoord = mw * Vector((coord[0]/ob.scale.x, coord[1]/ob.scale.y, coord[2]/ob.scale.z))
|
|
worldCoord = mw @ Vector((coord[0], coord[1], coord[2]))
|
|
minx = min(minx, worldCoord.x)
|
|
miny = min(miny, worldCoord.y)
|
|
minz = min(minz, worldCoord.z)
|
|
maxx = max(maxx, worldCoord.x)
|
|
maxy = max(maxy, worldCoord.y)
|
|
maxz = max(maxz, worldCoord.z)
|
|
for p in curve.points:
|
|
coord = p.co
|
|
# this can work badly with some imported curves, don't know why...
|
|
# worldCoord = mw * Vector((coord[0]/ob.scale.x, coord[1]/ob.scale.y, coord[2]/ob.scale.z))
|
|
worldCoord = mw @ Vector((coord[0], coord[1], coord[2]))
|
|
minx = min(minx, worldCoord.x)
|
|
miny = min(miny, worldCoord.y)
|
|
minz = min(minz, worldCoord.z)
|
|
maxx = max(maxx, worldCoord.x)
|
|
maxy = max(maxy, worldCoord.y)
|
|
maxz = max(maxz, worldCoord.z)
|
|
# progress(time.time()-t)
|
|
return minx, miny, minz, maxx, maxy, maxz
|
|
|
|
|
|
def getOperationSources(o):
|
|
if o.geometry_source == 'OBJECT':
|
|
# bpy.ops.object.select_all(action='DESELECT')
|
|
ob = bpy.data.objects[o.object_name]
|
|
o.objects = [ob]
|
|
ob.select_set(True)
|
|
bpy.context.view_layer.objects.active = ob
|
|
if o.enable_B or o.enable_A:
|
|
if o.old_rotation_A != o.rotation_A or o.old_rotation_B != o.rotation_B:
|
|
o.old_rotation_A = o.rotation_A
|
|
o.old_rotation_B = o.rotation_B
|
|
ob=bpy.data.objects[o.object_name]
|
|
ob.select_set(True)
|
|
bpy.context.view_layer.objects.active = ob
|
|
if o.A_along_x : #A parallel with X
|
|
if o.enable_A:
|
|
bpy.context.active_object.rotation_euler.x = o.rotation_A
|
|
if o.enable_B:
|
|
bpy.context.active_object.rotation_euler.y = o.rotation_B
|
|
else : #A parallel with Y
|
|
if o.enable_A:
|
|
bpy.context.active_object.rotation_euler.y = o.rotation_A
|
|
if o.enable_B:
|
|
bpy.context.active_object.rotation_euler.x = o.rotation_B
|
|
|
|
|
|
elif o.geometry_source == 'COLLECTION':
|
|
collection = bpy.data.collections[o.collection_name]
|
|
o.objects = collection.objects
|
|
elif o.geometry_source == 'IMAGE':
|
|
o.use_exact = False
|
|
|
|
if o.geometry_source == 'OBJECT' or o.geometry_source == 'COLLECTION':
|
|
o.onlycurves = True
|
|
for ob in o.objects:
|
|
if ob.type == 'MESH':
|
|
o.onlycurves = False
|
|
else:
|
|
o.onlycurves = False
|
|
|
|
def getBounds(o):
|
|
# print('kolikrat sem rpijde')
|
|
if o.geometry_source == 'OBJECT' or o.geometry_source == 'COLLECTION' or o.geometry_source == 'CURVE':
|
|
print("valid geometry")
|
|
minx, miny, minz, maxx, maxy, maxz = getBoundsWorldspace(o.objects, o.use_modifiers)
|
|
|
|
if o.minz_from_ob:
|
|
if minz == 10000000:
|
|
minz = 0
|
|
print("minz from object:" + str(minz))
|
|
o.min.z = minz
|
|
o.minz = o.min.z
|
|
else:
|
|
o.min.z = o.minz # max(bb[0][2]+l.z,o.minz)#
|
|
print("not minz from object")
|
|
|
|
if o.material_from_model:
|
|
print("material_from_model")
|
|
|
|
o.min.x = minx - o.material_radius_around_model
|
|
o.min.y = miny - o.material_radius_around_model
|
|
o.max.z = max(o.maxz, maxz)
|
|
|
|
|
|
o.max.x = maxx + o.material_radius_around_model
|
|
o.max.y = maxy + o.material_radius_around_model
|
|
else:
|
|
print("not material from model")
|
|
o.min.x = o.material_origin.x
|
|
o.min.y = o.material_origin.y
|
|
o.min.z = o.material_origin.z - o.material_size.z
|
|
o.max.x = o.min.x + o.material_size.x
|
|
o.max.y = o.min.y + o.material_size.y
|
|
o.max.z = o.material_origin.z
|
|
|
|
else:
|
|
i = bpy.data.images[o.source_image_name]
|
|
if o.source_image_crop:
|
|
sx = int(i.size[0] * o.source_image_crop_start_x / 100)
|
|
ex = int(i.size[0] * o.source_image_crop_end_x / 100)
|
|
sy = int(i.size[1] * o.source_image_crop_start_y / 100)
|
|
ey = int(i.size[1] * o.source_image_crop_end_y / 100)
|
|
# operation.image.resize(ex-sx,ey-sy)
|
|
crop = (sx, sy, ex, ey)
|
|
else:
|
|
sx = 0
|
|
ex = i.size[0]
|
|
sy = 0
|
|
ey = i.size[1]
|
|
|
|
o.pixsize = o.source_image_size_x / i.size[0]
|
|
|
|
o.min.x = o.source_image_offset.x + (sx) * o.pixsize
|
|
o.max.x = o.source_image_offset.x + (ex) * o.pixsize
|
|
o.min.y = o.source_image_offset.y + (sy) * o.pixsize
|
|
o.max.y = o.source_image_offset.y + (ey) * o.pixsize
|
|
o.min.z = o.source_image_offset.z + o.minz
|
|
o.max.z = o.source_image_offset.z
|
|
s = bpy.context.scene
|
|
m = s.cam_machine
|
|
if o.max.x - o.min.x > m.working_area.x or o.max.y - o.min.y > m.working_area.y or o.max.z - o.min.z > m.working_area.z:
|
|
# o.max.x=min(o.min.x+m.working_area.x,o.max.x)
|
|
# o.max.y=min(o.min.y+m.working_area.y,o.max.y)
|
|
# o.max.z=min(o.min.z+m.working_area.z,o.max.z)
|
|
o.warnings += 'Operation exceeds your machine limits\n'
|
|
|
|
# progress (o.min.x,o.min.y,o.min.z,o.max.x,o.max.y,o.max.z)
|
|
|
|
def getBoundsMultiple(operations):
|
|
"gets bounds of multiple operations, mainly for purpose of simulations or rest milling. highly suboptimal."
|
|
maxx = maxy = maxz = -10000000
|
|
minx = miny = minz = 10000000
|
|
for o in operations:
|
|
getBounds(o)
|
|
maxx = max(maxx, o.max.x)
|
|
maxy = max(maxy, o.max.y)
|
|
maxz = max(maxz, o.max.z)
|
|
minx = min(minx, o.min.x)
|
|
miny = min(miny, o.min.y)
|
|
minz = min(minz, o.min.z)
|
|
|
|
return minx, miny, minz, maxx, maxy, maxz
|
|
|
|
|
|
def samplePathLow(o, ch1, ch2, dosample):
|
|
v1 = Vector(ch1.points[-1])
|
|
v2 = Vector(ch2.points[0])
|
|
|
|
v = v2 - v1
|
|
d = v.length
|
|
v.normalize()
|
|
|
|
vref = Vector((0, 0, 0))
|
|
bpath = camPathChunk([])
|
|
i = 0
|
|
while vref.length < d:
|
|
i += 1
|
|
vref = v * o.dist_along_paths * i
|
|
if vref.length < d:
|
|
p = v1 + vref
|
|
bpath.points.append([p.x, p.y, p.z])
|
|
# print('between path')
|
|
# print(len(bpath))
|
|
pixsize = o.pixsize
|
|
if dosample:
|
|
if not (o.use_opencamlib and o.use_exact):
|
|
if o.use_exact:
|
|
if o.update_bullet_collision_tag:
|
|
prepareBulletCollision(o)
|
|
o.update_bullet_collision_tag = False
|
|
|
|
cutterdepth = o.cutter_shape.dimensions.z / 2
|
|
for p in bpath.points:
|
|
z = getSampleBullet(o.cutter_shape, p[0], p[1], cutterdepth, 1, o.minz)
|
|
if z > p[2]:
|
|
p[2] = z
|
|
else:
|
|
for p in bpath.points:
|
|
xs = (p[0] - o.min.x) / pixsize + o.borderwidth + pixsize / 2 # -m
|
|
ys = (p[1] - o.min.y) / pixsize + o.borderwidth + pixsize / 2 # -m
|
|
z = getSampleImage((xs, ys), o.offset_image, o.minz) + o.skin
|
|
if z > p[2]:
|
|
p[2] = z
|
|
return bpath
|
|
|
|
|
|
# def threadedSampling():#not really possible at all without running more blenders for same operation :( python!
|
|
# samples in both modes now - image and bullet collision too.
|
|
def sampleChunks(o, pathSamples, layers):
|
|
#
|
|
minx, miny, minz, maxx, maxy, maxz = o.min.x, o.min.y, o.min.z, o.max.x, o.max.y, o.max.z
|
|
getAmbient(o)
|
|
|
|
if o.use_exact: # prepare collision world
|
|
if o.use_opencamlib:
|
|
oclSample(o, pathSamples)
|
|
cutterdepth = 0
|
|
else:
|
|
if o.update_bullet_collision_tag:
|
|
prepareBulletCollision(o)
|
|
|
|
o.update_bullet_collision_tag = False
|
|
# print (o.ambient)
|
|
cutter = o.cutter_shape
|
|
cutterdepth = cutter.dimensions.z / 2
|
|
else:
|
|
if o.strategy != 'WATERLINE': # or prepare offset image, but not in some strategies.
|
|
prepareArea(o)
|
|
|
|
pixsize = o.pixsize
|
|
|
|
coordoffset = o.borderwidth + pixsize / 2 # -m
|
|
|
|
res = ceil(o.cutter_diameter / o.pixsize)
|
|
m = res / 2
|
|
|
|
t = time.time()
|
|
# print('sampling paths')
|
|
|
|
totlen = 0; # total length of all chunks, to estimate sampling time.
|
|
for ch in pathSamples:
|
|
totlen += len(ch.points)
|
|
layerchunks = []
|
|
minz = o.minz - 0.000001 # correction for image method problems
|
|
layeractivechunks = []
|
|
lastrunchunks = []
|
|
|
|
for l in layers:
|
|
layerchunks.append([])
|
|
layeractivechunks.append(camPathChunk([]))
|
|
lastrunchunks.append([])
|
|
|
|
zinvert = 0
|
|
if o.inverse:
|
|
ob = bpy.data.objects[o.object_name]
|
|
zinvert = ob.location.z + maxz # ob.bound_box[6][2]
|
|
|
|
n = 0
|
|
last_percent = -1
|
|
# timing for optimisation
|
|
samplingtime = timinginit()
|
|
sortingtime = timinginit()
|
|
totaltime = timinginit()
|
|
timingstart(totaltime)
|
|
lastz = minz
|
|
for patternchunk in pathSamples:
|
|
thisrunchunks = []
|
|
for l in layers:
|
|
thisrunchunks.append([])
|
|
lastlayer = None
|
|
currentlayer = None
|
|
lastsample = None
|
|
# threads_count=4
|
|
|
|
# for t in range(0,threads):
|
|
|
|
for s in patternchunk.points:
|
|
if o.strategy != 'WATERLINE' and int(100 * n / totlen) != last_percent:
|
|
last_percent = int(100 * n / totlen)
|
|
progress('sampling paths ', last_percent)
|
|
n += 1
|
|
x = s[0]
|
|
y = s[1]
|
|
if not o.ambient.contains(sgeometry.Point(x, y)):
|
|
newsample = (x, y, 1)
|
|
else:
|
|
if o.use_opencamlib and o.use_exact:
|
|
z = s[2]
|
|
if minz > z:
|
|
z = minz
|
|
newsample = (x, y, z)
|
|
####sampling
|
|
elif o.use_exact and not o.use_opencamlib:
|
|
|
|
if lastsample != None: # this is an optimalization, search only for near depths to the last sample. Saves about 30% of sampling time.
|
|
z = getSampleBullet(cutter, x, y, cutterdepth, 1,
|
|
lastsample[2] - o.dist_along_paths) # first try to the last sample
|
|
if z < minz - 1:
|
|
z = getSampleBullet(cutter, x, y, cutterdepth, lastsample[2] - o.dist_along_paths, minz)
|
|
else:
|
|
z = getSampleBullet(cutter, x, y, cutterdepth, 1, minz)
|
|
|
|
# print(z)
|
|
# here we have
|
|
else:
|
|
timingstart(samplingtime)
|
|
xs = (x - minx) / pixsize + coordoffset
|
|
ys = (y - miny) / pixsize + coordoffset
|
|
timingadd(samplingtime)
|
|
# if o.inverse:
|
|
# z=layerstart
|
|
z = getSampleImage((xs, ys), o.offset_image, minz) + o.skin
|
|
# if minz>z and o.ambient.isInside(x,y):
|
|
# z=minz;
|
|
################################
|
|
# handling samples
|
|
############################################
|
|
|
|
if minz > z:
|
|
z = minz
|
|
newsample = (x, y, z)
|
|
# z=max(minz,z)
|
|
|
|
# if sampled:# and (not o.inverse or (o.inverse)):uh what was this? disabled
|
|
# newsample=(x,y,z)
|
|
|
|
# elif o.ambient_behaviour=='ALL' and not o.inverse:#handle ambient here, this should be obsolete,
|
|
# newsample=(x,y,minz)
|
|
for i, l in enumerate(layers):
|
|
terminatechunk = False
|
|
|
|
ch = layeractivechunks[i]
|
|
# print(i,l)
|
|
# print(l[1],l[0])
|
|
|
|
if l[1] <= newsample[2] <= l[0]:
|
|
lastlayer = None # rather the last sample here ? has to be set to None, since sometimes lastsample vs lastlayer didn't fit and did ugly ugly stuff....
|
|
if lastsample != None:
|
|
for i2, l2 in enumerate(layers):
|
|
if l2[1] <= lastsample[2] <= l2[0]:
|
|
lastlayer = i2
|
|
|
|
currentlayer = i
|
|
if lastlayer != None and lastlayer != currentlayer: # and lastsample[2]!=newsample[2]:#sampling for sorted paths in layers- to go to the border of the sampled layer at least...there was a bug here, but should be fixed.
|
|
if currentlayer < lastlayer:
|
|
growing = True
|
|
r = range(currentlayer, lastlayer)
|
|
spliti = 1
|
|
else:
|
|
r = range(lastlayer, currentlayer)
|
|
growing = False
|
|
spliti = 0
|
|
# print(r)
|
|
li = 0
|
|
for ls in r:
|
|
splitz = layers[ls][1]
|
|
# print(ls)
|
|
|
|
v1 = lastsample
|
|
v2 = newsample
|
|
if o.protect_vertical:
|
|
v1, v2 = isVerticalLimit(v1, v2, o.protect_vertical_limit)
|
|
v1 = Vector(v1)
|
|
v2 = Vector(v2)
|
|
# print(v1,v2)
|
|
ratio = (splitz - v1.z) / (v2.z - v1.z)
|
|
# print(ratio)
|
|
betweensample = v1 + (v2 - v1) * ratio
|
|
|
|
# ch.points.append(betweensample.to_tuple())
|
|
|
|
if growing:
|
|
if li > 0:
|
|
layeractivechunks[ls].points.insert(-1, betweensample.to_tuple())
|
|
else:
|
|
layeractivechunks[ls].points.append(betweensample.to_tuple())
|
|
layeractivechunks[ls + 1].points.append(betweensample.to_tuple())
|
|
else:
|
|
# print(v1,v2,betweensample,lastlayer,currentlayer)
|
|
layeractivechunks[ls].points.insert(-1, betweensample.to_tuple())
|
|
layeractivechunks[ls + 1].points.insert(0, betweensample.to_tuple())
|
|
|
|
li += 1
|
|
# this chunk is terminated, and allready in layerchunks /
|
|
|
|
# ch.points.append(betweensample.to_tuple())#
|
|
ch.points.append(newsample)
|
|
elif l[1] > newsample[2]:
|
|
ch.points.append((newsample[0], newsample[1], l[1]))
|
|
elif l[0] < newsample[2]: # terminate chunk
|
|
terminatechunk = True
|
|
|
|
if terminatechunk:
|
|
if len(ch.points) > 0:
|
|
layerchunks[i].append(ch)
|
|
thisrunchunks[i].append(ch)
|
|
layeractivechunks[i] = camPathChunk([])
|
|
lastsample = newsample
|
|
|
|
for i, l in enumerate(layers):
|
|
ch = layeractivechunks[i]
|
|
if len(ch.points) > 0:
|
|
layerchunks[i].append(ch)
|
|
thisrunchunks[i].append(ch)
|
|
layeractivechunks[i] = camPathChunk([])
|
|
|
|
# PARENTING
|
|
if o.strategy == 'PARALLEL' or o.strategy == 'CROSS' or o.strategy == 'OUTLINEFILL':
|
|
timingstart(sortingtime)
|
|
parentChildDist(thisrunchunks[i], lastrunchunks[i], o)
|
|
timingadd(sortingtime)
|
|
|
|
lastrunchunks = thisrunchunks
|
|
|
|
# print(len(layerchunks[i]))
|
|
progress('checking relations between paths')
|
|
timingstart(sortingtime)
|
|
|
|
if o.strategy == 'PARALLEL' or o.strategy == 'CROSS' or o.strategy == 'OUTLINEFILL':
|
|
if len(layers) > 1: # sorting help so that upper layers go first always
|
|
for i in range(0, len(layers) - 1):
|
|
parents = []
|
|
children = []
|
|
# only pick chunks that should have connectivity assigned - 'last' and 'first' ones of the layer.
|
|
for ch in layerchunks[i + 1]:
|
|
if ch.children == []:
|
|
parents.append(ch)
|
|
for ch1 in layerchunks[i]:
|
|
if ch1.parents == []:
|
|
children.append(ch1)
|
|
|
|
parentChild(parents, children, o) # parent only last and first chunk, before it did this for all.
|
|
timingadd(sortingtime)
|
|
chunks = []
|
|
|
|
for i, l in enumerate(layers):
|
|
if o.ramp:
|
|
for ch in layerchunks[i]:
|
|
ch.zstart = layers[i][0]
|
|
ch.zend = layers[i][1]
|
|
chunks.extend(layerchunks[i])
|
|
timingadd(totaltime)
|
|
print(samplingtime)
|
|
print(sortingtime)
|
|
print(totaltime)
|
|
return chunks
|
|
|
|
|
|
def sampleChunksNAxis(o, pathSamples, layers):
|
|
#
|
|
minx, miny, minz, maxx, maxy, maxz = o.min.x, o.min.y, o.min.z, o.max.x, o.max.y, o.max.z
|
|
|
|
# prepare collision world
|
|
if o.update_bullet_collision_tag:
|
|
prepareBulletCollision(o)
|
|
# print('getting ambient')
|
|
getAmbient(o)
|
|
o.update_bullet_collision_tag = False
|
|
# print (o.ambient)
|
|
cutter = o.cutter_shape
|
|
cutterdepth = cutter.dimensions.z / 2
|
|
|
|
t = time.time()
|
|
print('sampling paths')
|
|
|
|
totlen = 0 # total length of all chunks, to estimate sampling time.
|
|
for chs in pathSamples:
|
|
totlen += len(chs.startpoints)
|
|
layerchunks = []
|
|
minz = o.minz
|
|
layeractivechunks = []
|
|
lastrunchunks = []
|
|
|
|
for l in layers:
|
|
layerchunks.append([])
|
|
layeractivechunks.append(camPathChunk([]))
|
|
lastrunchunks.append([])
|
|
n = 0
|
|
|
|
lastz = minz
|
|
for patternchunk in pathSamples:
|
|
# print (patternchunk.endpoints)
|
|
thisrunchunks = []
|
|
for l in layers:
|
|
thisrunchunks.append([])
|
|
lastlayer = None
|
|
currentlayer = None
|
|
lastsample = None
|
|
# threads_count=4
|
|
lastrotation = (0, 0, 0)
|
|
# for t in range(0,threads):
|
|
# print(len(patternchunk.startpoints),len( patternchunk.endpoints))
|
|
spl = len(patternchunk.startpoints)
|
|
for si in range(0,
|
|
spl): # ,startp in enumerate(patternchunk.startpoints):#TODO: seems we are writing into the source chunk , and that is why we need to write endpoints everywhere too?
|
|
|
|
if n / 200.0 == int(n / 200.0):
|
|
progress('sampling paths ', int(100 * n / totlen))
|
|
n += 1
|
|
sampled = False
|
|
# print(si)
|
|
|
|
# get the vector to sample
|
|
startp = Vector(patternchunk.startpoints[si])
|
|
endp = Vector(patternchunk.endpoints[si])
|
|
rotation = patternchunk.rotations[si]
|
|
sweepvect = endp - startp
|
|
sweepvect.normalize()
|
|
####sampling
|
|
if rotation != lastrotation:
|
|
|
|
cutter.rotation_euler = rotation
|
|
# cutter.rotation_euler.x=-cutter.rotation_euler.x
|
|
# print(rotation)
|
|
|
|
if o.cutter_type == 'VCARVE': # Bullet cone is always pointing Up Z in the object
|
|
cutter.rotation_euler.x += pi
|
|
cutter.update_tag()
|
|
# bpy.context.scene.frame_set(-1)
|
|
# bpy.context.scene.update()
|
|
# bpy.context.scene.frame_set(1)
|
|
bpy.context.scene.frame_set(
|
|
1) # this has to be :( it resets the rigidbody world. No other way to update it probably now :(
|
|
bpy.context.scene.frame_set(2) # actually 2 frame jumps are needed.
|
|
bpy.context.scene.frame_set(0)
|
|
#
|
|
#
|
|
# bpy.context.scene.frame_set(-1)
|
|
|
|
# bpy.context.scene.update()
|
|
# update scene here?
|
|
|
|
# print(startp,endp)
|
|
# samplestartp=startp+sweepvect*0.3#this is correction for the sweep algorithm to work better.
|
|
newsample = getSampleBulletNAxis(cutter, startp, endp, rotation, cutterdepth)
|
|
|
|
# print('totok',startp,endp,rotation,newsample)
|
|
################################
|
|
# handling samples
|
|
############################################
|
|
if newsample != None: # this is weird, but will leave it this way now.. just prototyping here.
|
|
sampled = True
|
|
else: # TODO: why was this here?
|
|
newsample = startp
|
|
sampled = True
|
|
# print(newsample)
|
|
|
|
# elif o.ambient_behaviour=='ALL' and not o.inverse:#handle ambient here
|
|
# newsample=(x,y,minz)
|
|
if sampled:
|
|
for i, l in enumerate(layers):
|
|
terminatechunk = False
|
|
ch = layeractivechunks[i]
|
|
|
|
# print(i,l)
|
|
# print(l[1],l[0])
|
|
v = startp - newsample
|
|
distance = -v.length
|
|
|
|
if l[1] <= distance <= l[0]:
|
|
lastlayer = currentlayer
|
|
currentlayer = i
|
|
|
|
if lastsample != None and lastlayer != None and currentlayer != None and lastlayer != currentlayer: # sampling for sorted paths in layers- to go to the border of the sampled layer at least...there was a bug here, but should be fixed.
|
|
if currentlayer < lastlayer:
|
|
growing = True
|
|
r = range(currentlayer, lastlayer)
|
|
spliti = 1
|
|
else:
|
|
r = range(lastlayer, currentlayer)
|
|
growing = False
|
|
spliti = 0
|
|
# print(r)
|
|
li = 0
|
|
|
|
for ls in r:
|
|
splitdistance = layers[ls][1]
|
|
|
|
# v1=lastsample
|
|
# v2=newsample
|
|
# if o.protect_vertical:#different algo for N-Axis! need sto be perpendicular to or whatever.
|
|
# v1,v2=isVerticalLimit(v1,v2,o.protect_vertical_limit)
|
|
# v1=Vector(v1)
|
|
# v2=Vector(v2)
|
|
# print(v1,v2)
|
|
ratio = (splitdistance - lastdistance) / (distance - lastdistance)
|
|
# print(ratio)
|
|
betweensample = lastsample + (newsample - lastsample) * ratio
|
|
# this probably doesn't work at all!!!! check this algoritm>
|
|
betweenrotation = tuple_add(lastrotation,
|
|
tuple_mul(tuple_sub(rotation, lastrotation), ratio))
|
|
# startpoint = retract point, it has to be always available...
|
|
betweenstartpoint = laststartpoint + (startp - laststartpoint) * ratio
|
|
# here, we need to have also possible endpoints always..
|
|
betweenendpoint = lastendpoint + (endp - lastendpoint) * ratio
|
|
if growing:
|
|
if li > 0:
|
|
layeractivechunks[ls].points.insert(-1, betweensample)
|
|
layeractivechunks[ls].rotations.insert(-1, betweenrotation)
|
|
layeractivechunks[ls].startpoints.insert(-1, betweenstartpoint)
|
|
layeractivechunks[ls].endpoints.insert(-1, betweenendpoint)
|
|
else:
|
|
layeractivechunks[ls].points.append(betweensample)
|
|
layeractivechunks[ls].rotations.append(betweenrotation)
|
|
layeractivechunks[ls].startpoints.append(betweenstartpoint)
|
|
layeractivechunks[ls].endpoints.append(betweenendpoint)
|
|
layeractivechunks[ls + 1].points.append(betweensample)
|
|
layeractivechunks[ls + 1].rotations.append(betweenrotation)
|
|
layeractivechunks[ls + 1].startpoints.append(betweenstartpoint)
|
|
layeractivechunks[ls + 1].endpoints.append(betweenendpoint)
|
|
else:
|
|
|
|
layeractivechunks[ls].points.insert(-1, betweensample)
|
|
layeractivechunks[ls].rotations.insert(-1, betweenrotation)
|
|
layeractivechunks[ls].startpoints.insert(-1, betweenstartpoint)
|
|
layeractivechunks[ls].endpoints.insert(-1, betweenendpoint)
|
|
|
|
layeractivechunks[ls + 1].points.append(betweensample)
|
|
layeractivechunks[ls + 1].rotations.append(betweenrotation)
|
|
layeractivechunks[ls + 1].startpoints.append(betweenstartpoint)
|
|
layeractivechunks[ls + 1].endpoints.append(betweenendpoint)
|
|
|
|
# layeractivechunks[ls+1].points.insert(0,betweensample)
|
|
li += 1
|
|
# this chunk is terminated, and allready in layerchunks /
|
|
|
|
# ch.points.append(betweensample)#
|
|
ch.points.append(newsample)
|
|
ch.rotations.append(rotation)
|
|
ch.startpoints.append(startp)
|
|
ch.endpoints.append(endp)
|
|
lastdistance = distance
|
|
|
|
|
|
elif l[1] > distance:
|
|
v = sweepvect * l[1]
|
|
p = startp - v
|
|
ch.points.append(p)
|
|
ch.rotations.append(rotation)
|
|
ch.startpoints.append(startp)
|
|
ch.endpoints.append(endp)
|
|
elif l[0] < distance: # retract to original track
|
|
ch.points.append(startp)
|
|
ch.rotations.append(rotation)
|
|
ch.startpoints.append(startp)
|
|
ch.endpoints.append(endp)
|
|
# terminatechunk=True
|
|
|
|
# if terminatechunk:
|
|
# #print(ch.points)
|
|
# if len(ch.points)>0:
|
|
# if len(ch.points)>0:
|
|
# layerchunks[i].append(ch)
|
|
# thisrunchunks[i].append(ch)
|
|
# layeractivechunks[i]=camPathChunk([])
|
|
|
|
# else:
|
|
# terminatechunk=True
|
|
lastsample = newsample
|
|
lastrotation = rotation
|
|
laststartpoint = startp
|
|
lastendpoint = endp
|
|
|
|
for i, l in enumerate(layers):
|
|
ch = layeractivechunks[i]
|
|
if len(ch.points) > 0:
|
|
layerchunks[i].append(ch)
|
|
thisrunchunks[i].append(ch)
|
|
layeractivechunks[i] = camPathChunk([])
|
|
|
|
if (o.strategy == 'PARALLEL' or o.strategy == 'CROSS' or o.strategy == 'OUTLINEFILL'):
|
|
parentChildDist(thisrunchunks[i], lastrunchunks[i], o)
|
|
|
|
lastrunchunks = thisrunchunks
|
|
|
|
# print(len(layerchunks[i]))
|
|
|
|
progress('checking relations between paths')
|
|
"""#this algorithm should also work for n-axis, but now is "sleeping"
|
|
if (o.strategy=='PARALLEL' or o.strategy=='CROSS'):
|
|
if len(layers)>1:# sorting help so that upper layers go first always
|
|
for i in range(0,len(layers)-1):
|
|
#print('layerstuff parenting')
|
|
parentChild(layerchunks[i+1],layerchunks[i],o)
|
|
"""
|
|
chunks = []
|
|
for i, l in enumerate(layers):
|
|
chunks.extend(layerchunks[i])
|
|
|
|
return chunks
|
|
|
|
def extendChunks5axis(chunks, o):
|
|
s = bpy.context.scene
|
|
m = s.cam_machine
|
|
s = bpy.context.scene
|
|
free_movement_height = o.free_movement_height # o.max.z +
|
|
if m.use_position_definitions: # dhull
|
|
cutterstart = Vector((m.starting_position.x, m.starting_position.y,
|
|
max(o.max.z, m.starting_position.z))) # start point for casting
|
|
else:
|
|
cutterstart = Vector((0, 0, max(o.max.z, free_movement_height))) # start point for casting
|
|
cutterend = Vector((0, 0, o.min.z))
|
|
oriname = o.name + ' orientation'
|
|
ori = s.objects[oriname]
|
|
# rotationaxes = rotTo2axes(ori.rotation_euler,'CA')#warning-here it allready is reset to 0!!
|
|
print('rot', o.rotationaxes)
|
|
a, b = o.rotationaxes # this is all nonsense by now.
|
|
for chunk in chunks:
|
|
for v in chunk.points:
|
|
cutterstart.x = v[0]
|
|
cutterstart.y = v[1]
|
|
cutterend.x = v[0]
|
|
cutterend.y = v[1]
|
|
chunk.startpoints.append(cutterstart.to_tuple())
|
|
chunk.endpoints.append(cutterend.to_tuple())
|
|
chunk.rotations.append(
|
|
(a, b, 0)) # TODO: this is a placeholder. It does 99.9% probably write total nonsense.
|
|
|
|
|
|
|
|
def curveToShapely(cob, use_modifiers=False):
|
|
chunks = curveToChunks(cob, use_modifiers)
|
|
polys = chunksToShapely(chunks)
|
|
return polys
|
|
|
|
|
|
# separate function in blender, so you can offset any curve.
|
|
# FIXME: same algorithms as the cutout strategy, because that is hierarchy-respecting.
|
|
|
|
def silhoueteOffset(context, offset):
|
|
bpy.context.scene.cursor.location = (0, 0, 0)
|
|
ob = bpy.context.active_object
|
|
if ob.type == 'CURVE' or ob.type == 'FONT':
|
|
silhs = curveToShapely(ob)
|
|
else:
|
|
silhs = getObjectSilhouete('OBJECTS', [ob])
|
|
|
|
polys = []
|
|
mp = shapely.ops.unary_union(silhs)
|
|
mp = mp.buffer(offset, resolution=64)
|
|
shapelyToCurve('offset curve', mp, ob.location.z)
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
def polygonBoolean(context, boolean_type):
|
|
bpy.context.scene.cursor.location = (0, 0, 0)
|
|
ob = bpy.context.active_object
|
|
obs = []
|
|
for ob1 in bpy.context.selected_objects:
|
|
if ob1 != ob:
|
|
obs.append(ob1)
|
|
plist = curveToShapely(ob)
|
|
p1 = sgeometry.asMultiPolygon(plist)
|
|
polys = []
|
|
for o in obs:
|
|
plist = curveToShapely(o)
|
|
p2 = sgeometry.asMultiPolygon(plist)
|
|
polys.append(p2)
|
|
# print(polys)
|
|
if boolean_type == 'UNION':
|
|
for p2 in polys:
|
|
p1 = p1.union(p2)
|
|
elif boolean_type == 'DIFFERENCE':
|
|
for p2 in polys:
|
|
p1 = p1.difference(p2)
|
|
elif boolean_type == 'INTERSECT':
|
|
for p2 in polys:
|
|
p1 = p1.intersection(p2)
|
|
|
|
shapelyToCurve('boolean', p1, ob.location.z)
|
|
# bpy.ops.object.convert(target='CURVE')
|
|
# bpy.context.scene.cursor_location=ob.location
|
|
# bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
def Helix(r, np, zstart, pend, rev):
|
|
c = []
|
|
pi = math.pi
|
|
v = mathutils.Vector((r, 0, zstart))
|
|
e = mathutils.Euler((0, 0, 2.0 * pi / np))
|
|
zstep = (zstart - pend[2]) / (np * rev)
|
|
for a in range(0, int(np * rev)):
|
|
c.append((v.x + pend[0], v.y + pend[1], zstart - (a * zstep)))
|
|
v.rotate(e)
|
|
c.append((v.x + pend[0], v.y + pend[1], pend[2]))
|
|
|
|
return c
|
|
|
|
|
|
def comparezlevel(x):
|
|
return x[5]
|
|
|
|
|
|
def overlaps(bb1, bb2): # true if bb1 is child of bb2
|
|
ch1 = bb1
|
|
ch2 = bb2
|
|
if (ch2[1] > ch1[1] > ch1[0] > ch2[0] and ch2[3] > ch1[3] > ch1[2] > ch2[2]):
|
|
return True
|
|
|
|
|
|
def connectChunksLow(chunks, o):
|
|
""" connects chunks that are close to each other without lifting, sampling them 'low' """
|
|
if not o.stay_low or (o.strategy == 'CARVE' and o.carve_depth > 0):
|
|
return chunks
|
|
|
|
connectedchunks = []
|
|
chunks_to_resample = [] # for OpenCAMLib sampling
|
|
mergedist = 3 * o.dist_between_paths
|
|
if o.strategy == 'PENCIL': # this is bigger for pencil path since it goes on the surface to clean up the rests, and can go to close points on the surface without fear of going deep into material.
|
|
mergedist = 10 * o.dist_between_paths
|
|
|
|
if o.strategy == 'MEDIAL_AXIS':
|
|
mergedist = 1 * o.medial_axis_subdivision
|
|
|
|
if o.parallel_step_back:
|
|
mergedist *= 2
|
|
|
|
if o.merge_dist > 0:
|
|
mergedist = o.merge_dist
|
|
# mergedist=10
|
|
lastch = None
|
|
i = len(chunks)
|
|
pos = (0, 0, 0)
|
|
|
|
for ch in chunks:
|
|
if len(ch.points) > 0:
|
|
if lastch != None and (ch.distStart(pos, o) < mergedist):
|
|
# CARVE should lift allways, when it goes below surface...
|
|
# print(mergedist,ch.dist(pos,o))
|
|
if o.strategy == 'PARALLEL' or o.strategy == 'CROSS' or o.strategy == 'PENCIL': # for these paths sorting happens after sampling, thats why they need resample the connection
|
|
between = samplePathLow(o, lastch, ch, True)
|
|
else:
|
|
# print('addbetwee')
|
|
between = samplePathLow(o, lastch, ch,
|
|
False) # other paths either dont use sampling or are sorted before it.
|
|
if o.use_opencamlib and o.use_exact and (
|
|
o.strategy == 'PARALLEL' or o.strategy == 'CROSS' or o.strategy == 'PENCIL'):
|
|
chunks_to_resample.append(
|
|
(connectedchunks[-1], len(connectedchunks[-1].points), len(between.points)))
|
|
|
|
connectedchunks[-1].points.extend(between.points)
|
|
connectedchunks[-1].points.extend(ch.points)
|
|
else:
|
|
connectedchunks.append(ch)
|
|
lastch = ch
|
|
pos = lastch.points[-1]
|
|
|
|
if o.use_opencamlib and o.use_exact and o.strategy != 'CUTOUT' and o.strategy != 'POCKET':
|
|
oclResampleChunks(o, chunks_to_resample)
|
|
|
|
return connectedchunks
|
|
|
|
|
|
def getClosest(o, pos, chunks):
|
|
# ch=-1
|
|
mind = 10000
|
|
d = 100000000000
|
|
ch = None
|
|
for chtest in chunks:
|
|
cango = True
|
|
for child in chtest.children: # here was chtest.getNext==chtest, was doing recursion error and slowing down.
|
|
if child.sorted == False:
|
|
cango = False
|
|
break;
|
|
if cango:
|
|
d = chtest.dist(pos, o)
|
|
if d < mind:
|
|
ch = chtest
|
|
mind = d
|
|
return ch
|
|
|
|
|
|
def sortChunks(chunks, o):
|
|
if o.strategy != 'WATERLINE':
|
|
progress('sorting paths')
|
|
sys.setrecursionlimit(100000) # the getNext() function of CamPathChunk was running out of recursion limits.
|
|
sortedchunks = []
|
|
chunks_to_resample = []
|
|
|
|
lastch = None
|
|
i = len(chunks)
|
|
pos = (0, 0, 0)
|
|
# for ch in chunks:
|
|
# ch.getNext()#this stores the unsortedchildren properties
|
|
# print('numofchunks')
|
|
# print(len(chunks))
|
|
while len(chunks) > 0:
|
|
ch = None
|
|
if len(sortedchunks) == 0 or len(
|
|
lastch.parents) == 0: # first chunk or when there are no parents -> parents come after children here...
|
|
ch = getClosest(o, pos, chunks)
|
|
elif len(lastch.parents) > 0: # looks in parents for next candidate, recursively
|
|
# get siblings here
|
|
# siblings=[]
|
|
# for chs in lastch.parents:
|
|
# siblings.extend(chs.children)
|
|
# ch = getClosest(o,pos,siblings)
|
|
# if ch==None:
|
|
# ch = getClosest(o,pos,chunks)
|
|
for parent in lastch.parents:
|
|
ch = parent.getNextClosest(o, pos)
|
|
if ch != None:
|
|
break
|
|
if ch == None:
|
|
ch = getClosest(o, pos, chunks)
|
|
# break
|
|
# pass;
|
|
if ch is not None: # found next chunk, append it to list
|
|
# only adaptdist the chunk if it has not been sorted before
|
|
if not ch.sorted:
|
|
ch.adaptdist(pos, o)
|
|
ch.sorted = True
|
|
# print(len(ch.parents),'children')
|
|
chunks.remove(ch)
|
|
sortedchunks.append(ch)
|
|
lastch = ch
|
|
pos = lastch.points[-1]
|
|
# print(i, len(chunks))
|
|
# experimental fix for infinite loop problem
|
|
# else:
|
|
# THIS PROBLEM WASN'T HERE AT ALL. but keeping it here, it might fix the problems somwhere else:)
|
|
# can't find chunks close enough and still some chunks left
|
|
# to be sorted. For now just move the remaining chunks over to
|
|
# the sorted list.
|
|
# This fixes an infinite loop condition that occurs sometimes.
|
|
# This is a bandaid fix: need to find the root cause of this problem
|
|
# suspect it has to do with the sorted flag?
|
|
# print("no chunks found closest. Chunks not sorted: ", len(chunks))
|
|
# sortedchunks.extend(chunks)
|
|
# chunks[:] = []
|
|
|
|
i -= 1
|
|
|
|
sys.setrecursionlimit(1000)
|
|
if o.strategy != 'DRILL' and o.strategy != 'OUTLINEFILL': # THIS SHOULD AVOID ACTUALLY MOST STRATEGIES, THIS SHOULD BE DONE MANUALLY, BECAUSE SOME STRATEGIES GET SORTED TWICE.
|
|
sortedchunks = connectChunksLow(sortedchunks, o)
|
|
return sortedchunks
|
|
|
|
|
|
def getVectorRight(lastv, verts): # most right vector from a set regarding angle..
|
|
defa = 100
|
|
v1 = Vector(lastv[0])
|
|
v2 = Vector(lastv[1])
|
|
va = v2 - v1
|
|
for i, v in enumerate(verts):
|
|
if v != lastv[0]:
|
|
vb = Vector(v) - v2
|
|
a = va.angle_signed(Vector(vb))
|
|
# if a<=0:
|
|
# a=2*pi+a
|
|
|
|
if a < defa:
|
|
defa = a
|
|
returnvec = i
|
|
return returnvec
|
|
|
|
|
|
def cleanUpDict(ndict):
|
|
print('removing lonely points') # now it should delete all junk first, iterate over lonely verts.
|
|
# found_solitaires=True
|
|
# while found_solitaires:
|
|
found_solitaires = False
|
|
keys = []
|
|
keys.extend(ndict.keys())
|
|
removed = 0
|
|
for k in keys:
|
|
print(k)
|
|
print(ndict[k])
|
|
if len(ndict[k]) <= 1:
|
|
newcheck = [k]
|
|
while (len(newcheck) > 0):
|
|
v = newcheck.pop()
|
|
if len(ndict[v]) <= 1:
|
|
for v1 in ndict[v]:
|
|
newcheck.append(v)
|
|
dictRemove(ndict, v)
|
|
removed += 1
|
|
found_solitaires = True
|
|
print(removed)
|
|
|
|
|
|
def dictRemove(dict, val):
|
|
for v in dict[val]:
|
|
dict[v].remove(val)
|
|
dict.pop(val)
|
|
|
|
|
|
def addLoop(parentloop, start, end):
|
|
added = False
|
|
for l in parentloop[2]:
|
|
if l[0] < start and l[1] > end:
|
|
addLoop(l, start, end)
|
|
return
|
|
parentloop[2].append([start, end, []])
|
|
|
|
|
|
def cutloops(csource, parentloop, loops):
|
|
copy = csource[parentloop[0]:parentloop[1]]
|
|
|
|
for li in range(len(parentloop[2]) - 1, -1, -1):
|
|
l = parentloop[2][li]
|
|
# print(l)
|
|
copy = copy[:l[0] - parentloop[0]] + copy[l[1] - parentloop[0]:]
|
|
loops.append(copy)
|
|
for l in parentloop[2]:
|
|
cutloops(csource, l, loops)
|
|
|
|
|
|
def getOperationSilhouete(operation):
|
|
"""gets silhouete for the operation
|
|
uses image thresholding for everything except curves.
|
|
"""
|
|
if operation.update_silhouete_tag:
|
|
image = None
|
|
objects = None
|
|
if operation.geometry_source == 'OBJECT' or operation.geometry_source == 'COLLECTION':
|
|
if operation.onlycurves == False:
|
|
stype = 'OBJECTS'
|
|
else:
|
|
stype = 'CURVES'
|
|
else:
|
|
stype = 'IMAGE'
|
|
|
|
totfaces = 0
|
|
if stype == 'OBJECTS':
|
|
for ob in operation.objects:
|
|
if ob.type == 'MESH':
|
|
totfaces += len(ob.data.polygons)
|
|
|
|
if (stype == 'OBJECTS' and totfaces > 200000) or stype == 'IMAGE':
|
|
print('image method')
|
|
samples = renderSampleImage(operation)
|
|
if stype == 'OBJECTS':
|
|
i = samples > operation.minz - 0.0000001 # numpy.min(operation.zbuffer_image)-0.0000001##the small number solves issue with totally flat meshes, which people tend to mill instead of proper pockets. then the minimum was also maximum, and it didn't detect contour.
|
|
else:
|
|
i = samples > numpy.min(operation.zbuffer_image) # this fixes another numeric imprecision.
|
|
|
|
chunks = imageToChunks(operation, i)
|
|
operation.silhouete = chunksToShapely(chunks)
|
|
# print(operation.silhouete)
|
|
# this conversion happens because we need the silh to be oriented, for milling directions.
|
|
else:
|
|
print('object method for retrieving silhouette') #
|
|
operation.silhouete = getObjectSilhouete(stype, objects=operation.objects)
|
|
|
|
operation.update_silhouete_tag = False
|
|
return operation.silhouete
|
|
|
|
|
|
def getObjectSilhouete(stype, objects=None, use_modifiers=False):
|
|
# o=operation
|
|
if stype == 'CURVES': # curve conversion to polygon format
|
|
allchunks = []
|
|
for ob in objects:
|
|
chunks = curveToChunks(ob)
|
|
allchunks.extend(chunks)
|
|
silhouete = chunksToShapely(allchunks)
|
|
|
|
elif stype == 'OBJECTS':
|
|
totfaces = 0
|
|
for ob in objects:
|
|
totfaces += len(ob.data.polygons)
|
|
|
|
if totfaces < 20000000: # boolean polygons method originaly was 20 000 poly limit, now limitless, it might become teribly slow, but who cares?
|
|
t = time.time()
|
|
print('shapely getting silhouette')
|
|
polys = []
|
|
for ob in objects:
|
|
|
|
if use_modifiers:
|
|
m = ob.to_mesh(preserve_all_data_layers=True, depsgraph=bpy.context.evaluated_depsgraph_get())
|
|
else:
|
|
m = ob.data
|
|
mw = ob.matrix_world
|
|
mwi = mw.inverted()
|
|
r = ob.rotation_euler
|
|
m.calc_loop_triangles()
|
|
id = 0
|
|
e = 0.000001
|
|
scaleup = 100
|
|
for tri in m.loop_triangles:
|
|
n = tri.normal.copy()
|
|
n.rotate(r)
|
|
# verts=[]
|
|
# for i in f.vertices:
|
|
# verts.append(mw*m.vertices[i].co)
|
|
# n=mathutils.geometry.normal(verts[0],verts[1],verts[2])
|
|
if tri.area > 0 and n.z != 0: # n.z>0.0 and f.area>0.0 :
|
|
s = []
|
|
c = mw @ tri.center
|
|
c = c.xy
|
|
for vert_index in tri.vertices:
|
|
v = mw @ m.vertices[vert_index].co
|
|
s.append((v.x, v.y))
|
|
if len(s) > 2:
|
|
# print(s)
|
|
p = spolygon.Polygon(s)
|
|
# print(dir(p))
|
|
if p.is_valid:
|
|
# polys.append(p)
|
|
polys.append(p.buffer(e, resolution=0))
|
|
# if id==923:
|
|
# m.polygons[923].select
|
|
id += 1
|
|
if use_modifiers:
|
|
bpy.data.meshes.remove(m)
|
|
# print(polys
|
|
if totfaces < 20000:
|
|
p = sops.unary_union(polys)
|
|
else:
|
|
print('computing in parts')
|
|
bigshapes = []
|
|
i = 1
|
|
part = 20000
|
|
while i * part < totfaces:
|
|
print(i)
|
|
ar = polys[(i - 1) * part:i * part]
|
|
bigshapes.append(sops.unary_union(ar))
|
|
i += 1
|
|
if (i - 1) * part < totfaces:
|
|
last_ar = polys[(i - 1) * part:]
|
|
bigshapes.append(sops.unary_union(last_ar))
|
|
print('joining')
|
|
p = sops.unary_union(bigshapes)
|
|
|
|
print(time.time() - t)
|
|
|
|
t = time.time()
|
|
silhouete = [p] # [polygon_utils_cam.Shapely2Polygon(p)]
|
|
|
|
return silhouete
|
|
|
|
|
|
def getAmbient(o):
|
|
if o.update_ambient_tag:
|
|
if o.ambient_cutter_restrict: # cutter stays in ambient & limit curve
|
|
m = o.cutter_diameter / 2
|
|
else:
|
|
m = 0
|
|
|
|
if o.ambient_behaviour == 'AROUND':
|
|
r = o.ambient_radius - m
|
|
o.ambient = getObjectOutline(r, o, True) # in this method we need ambient from silhouete
|
|
else:
|
|
o.ambient = spolygon.Polygon(((o.min.x + m, o.min.y + m), (o.min.x + m, o.max.y - m),
|
|
(o.max.x - m, o.max.y - m), (o.max.x - m, o.min.y + m)))
|
|
|
|
if o.use_limit_curve:
|
|
if o.limit_curve != '':
|
|
limit_curve = bpy.data.objects[o.limit_curve]
|
|
# polys=curveToPolys(limit_curve)
|
|
polys = curveToShapely(limit_curve)
|
|
o.limit_poly = shapely.ops.unary_union(polys)
|
|
# for p in polys:
|
|
# o.limit_poly+=p
|
|
if o.ambient_cutter_restrict:
|
|
o.limit_poly = o.limit_poly.buffer(o.cutter_diameter / 2, resolution=o.circle_detail)
|
|
o.ambient = o.ambient.intersection(o.limit_poly)
|
|
o.update_ambient_tag = False
|
|
|
|
|
|
def getObjectOutline(radius, o, Offset): # FIXME: make this one operation independent
|
|
# circle detail, optimize, optimize thresold.
|
|
|
|
polygons = getOperationSilhouete(o)
|
|
|
|
i = 0
|
|
# print('offseting polygons')
|
|
|
|
if Offset:
|
|
offset = 1
|
|
else:
|
|
offset = -1
|
|
|
|
outlines = []
|
|
i = 0
|
|
# print(polygons, polygons.type)
|
|
for p1 in polygons: # sort by size before this???
|
|
print(p1.type, len(polygons))
|
|
i += 1
|
|
if radius > 0:
|
|
p1 = p1.buffer(radius * offset, resolution=o.circle_detail)
|
|
outlines.append(p1)
|
|
|
|
print(outlines)
|
|
if o.dont_merge:
|
|
outline = sgeometry.MultiPolygon(outlines)
|
|
# for ci in range(0,len(p)):
|
|
# outline.addContour(p[ci],p.isHole(ci))
|
|
else:
|
|
# print(p)
|
|
outline = shapely.ops.unary_union(outlines)
|
|
# outline = sgeometry.MultiPolygon([outline])
|
|
# shapelyToCurve('oboutline',outline,0)
|
|
return outline
|
|
|
|
|
|
def addOrientationObject(o):
|
|
"""the orientation object should be used to set up orientations of the object for 4 and 5 axis milling."""
|
|
name = o.name + ' orientation'
|
|
s = bpy.context.scene
|
|
if s.objects.find(name) == -1:
|
|
bpy.ops.object.empty_add(type='ARROWS', align='WORLD', location=(0, 0, 0))
|
|
|
|
ob = bpy.context.active_object
|
|
ob.empty_draw_size = 0.05
|
|
ob.show_name = True
|
|
ob.name = name
|
|
ob = s.objects[name]
|
|
if o.machine_axes == '4':
|
|
|
|
if o.rotary_axis_1 == 'X':
|
|
ob.lock_rotation = [False, True, True]
|
|
ob.rotation_euler[1] = 0
|
|
ob.rotation_euler[2] = 0
|
|
if o.rotary_axis_1 == 'Y':
|
|
ob.lock_rotation = [True, False, True]
|
|
ob.rotation_euler[0] = 0
|
|
ob.rotation_euler[2] = 0
|
|
if o.rotary_axis_1 == 'Z':
|
|
ob.lock_rotation = [True, True, False]
|
|
ob.rotation_euler[0] = 0
|
|
ob.rotation_euler[1] = 0
|
|
elif o.machine_axes == '5':
|
|
ob.lock_rotation = [False, False, True]
|
|
|
|
ob.rotation_euler[2] = 0 # this will be a bit hard to rotate.....
|
|
|
|
|
|
# def addCutterOrientationObject(o):
|
|
|
|
|
|
def removeOrientationObject(o): # not working
|
|
name = o.name + ' orientation'
|
|
if bpy.context.scene.objects.find(name) > -1:
|
|
ob = bpy.context.scene.objects[name]
|
|
delob(ob)
|
|
|
|
|
|
def addTranspMat(ob, mname, color, alpha):
|
|
if mname in bpy.data.materials:
|
|
mat = bpy.data.materials[mname]
|
|
else:
|
|
mat = bpy.data.materials.new(name=mname)
|
|
mat.use_nodes = True
|
|
bsdf = mat.node_tree.nodes["Principled BSDF"]
|
|
|
|
# Assign it to object
|
|
if ob.data.materials:
|
|
ob.data.materials[0] = mat
|
|
else:
|
|
ob.data.materials.append(mat)
|
|
|
|
def addMachineAreaObject():
|
|
s = bpy.context.scene
|
|
ao = bpy.context.active_object
|
|
if s.objects.get('CAM_machine') is not None:
|
|
o = s.objects['CAM_machine']
|
|
else:
|
|
oldunits = s.unit_settings.system
|
|
# need to be in metric units when adding machine mesh object
|
|
# in order for location to work properly
|
|
s.unit_settings.system = 'METRIC'
|
|
bpy.ops.mesh.primitive_cube_add(align='WORLD', enter_editmode=False, location=(1, 1, -1), rotation=(0, 0, 0))
|
|
o = bpy.context.active_object
|
|
o.name = 'CAM_machine'
|
|
o.data.name = 'CAM_machine'
|
|
bpy.ops.object.transform_apply(location=True, rotation=False, scale=False)
|
|
# o.type = 'SOLID'
|
|
bpy.ops.object.editmode_toggle()
|
|
bpy.ops.mesh.delete(type='ONLY_FACE')
|
|
bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE', action='TOGGLE')
|
|
bpy.ops.mesh.select_all(action='TOGGLE')
|
|
bpy.ops.mesh.subdivide(number_cuts=32, smoothness=0, quadcorner='STRAIGHT_CUT', fractal=0,
|
|
fractal_along_normal=0, seed=0)
|
|
bpy.ops.mesh.select_nth(nth=2, offset=0)
|
|
bpy.ops.mesh.delete(type='EDGE')
|
|
bpy.ops.mesh.primitive_cube_add(align='WORLD', enter_editmode=False, location=(1, 1, -1), rotation=(0, 0, 0))
|
|
|
|
bpy.ops.object.editmode_toggle()
|
|
# addTranspMat(o, "violet_transparent", (0.800000, 0.530886, 0.725165), 0.1)
|
|
o.display_type = 'BOUNDS'
|
|
o.hide_render = True
|
|
o.hide_select = True
|
|
# o.select = False
|
|
s.unit_settings.system = oldunits
|
|
|
|
# bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
|
|
|
|
o.dimensions = bpy.context.scene.cam_machine.working_area
|
|
if ao is not None:
|
|
ao.select_set(True)
|
|
# else:
|
|
# bpy.context.scene.objects.active = None
|
|
|
|
|
|
def addMaterialAreaObject():
|
|
s = bpy.context.scene
|
|
operation = s.cam_operations[s.cam_active_operation]
|
|
getOperationSources(operation)
|
|
getBounds(operation)
|
|
|
|
ao = bpy.context.active_object
|
|
if s.objects.get('CAM_material') is not None:
|
|
o = s.objects['CAM_material']
|
|
else:
|
|
bpy.ops.mesh.primitive_cube_add(align='WORLD', enter_editmode=False, location=(1, 1, -1), rotation=(0, 0, 0))
|
|
o = bpy.context.active_object
|
|
o.name = 'CAM_material'
|
|
o.data.name = 'CAM_material'
|
|
bpy.ops.object.transform_apply(location=True, rotation=False, scale=False)
|
|
|
|
# addTranspMat(o, 'blue_transparent', (0.458695, 0.794658, 0.8), 0.1)
|
|
o.display_type = 'BOUNDS'
|
|
o.hide_render = True
|
|
o.hide_select = True
|
|
o.select_set(state=True, view_layer=None)
|
|
# bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
|
|
|
|
o.dimensions = bpy.context.scene.cam_machine.working_area
|
|
|
|
o.dimensions = (
|
|
operation.max.x - operation.min.x, operation.max.y - operation.min.y, operation.max.z - operation.min.z)
|
|
o.location = (operation.min.x, operation.min.y, operation.max.z)
|
|
if ao is not None:
|
|
ao.select_set(True)
|
|
# else:
|
|
# bpy.context.scene.objects.active = None
|
|
|
|
|
|
def getContainer():
|
|
s = bpy.context.scene
|
|
if s.objects.get('CAM_OBJECTS') == None:
|
|
bpy.ops.object.empty_add(type='PLAIN_AXES', align='WORLD')
|
|
container = bpy.context.active_object
|
|
container.name = 'CAM_OBJECTS'
|
|
container.location = [0, 0, 0]
|
|
container.hide = True
|
|
else:
|
|
container = s.objects['CAM_OBJECTS']
|
|
|
|
return container
|
|
|
|
# progress('finished')
|
|
|
|
# tools for voroni graphs all copied from the delaunayVoronoi addon:
|
|
class Point:
|
|
def __init__(self, x, y, z):
|
|
self.x, self.y, self.z = x, y, z
|
|
|
|
def unique(L):
|
|
"""Return a list of unhashable elements in s, but without duplicates.
|
|
[[1, 2], [2, 3], [1, 2]] >>> [[1, 2], [2, 3]]"""
|
|
# For unhashable objects, you can sort the sequence and then scan from the end of the list, deleting duplicates as you go
|
|
nDupli = 0
|
|
nZcolinear = 0
|
|
L.sort() # sort() brings the equal elements together; then duplicates are easy to weed out in a single pass.
|
|
last = L[-1]
|
|
for i in range(len(L) - 2, -1, -1):
|
|
if last[:2] == L[i][:2]: # XY coordinates compararison
|
|
if last[2] == L[i][2]: # Z coordinates compararison
|
|
nDupli += 1 # duplicates vertices
|
|
else: # Z colinear
|
|
nZcolinear += 1
|
|
del L[i]
|
|
else:
|
|
last = L[i]
|
|
return (nDupli,
|
|
nZcolinear) # list data type is mutable, input list will automatically update and doesn't need to be returned
|
|
|
|
|
|
def checkEqual(lst):
|
|
return lst[1:] == lst[:-1]
|
|
|
|
|
|
def prepareIndexed(o):
|
|
s = bpy.context.scene
|
|
# first store objects positions/rotations
|
|
o.matrices = []
|
|
o.parents = []
|
|
for ob in o.objects:
|
|
o.matrices.append(ob.matrix_world.copy())
|
|
o.parents.append(ob.parent)
|
|
|
|
# then rotate them
|
|
for ob in o.objects:
|
|
ob.select = True
|
|
s.objects.active = ob
|
|
bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM')
|
|
|
|
s.cursor_location = (0, 0, 0)
|
|
oriname = o.name + ' orientation'
|
|
ori = s.objects[oriname]
|
|
o.orientation_matrix = ori.matrix_world.copy()
|
|
o.rotationaxes = rotTo2axes(ori.rotation_euler, 'CA')
|
|
ori.select = True
|
|
s.objects.active = ori
|
|
# we parent all objects to the orientation object
|
|
bpy.ops.object.parent_set(type='OBJECT', keep_transform=True)
|
|
for ob in o.objects:
|
|
ob.select = False
|
|
# then we move the orientation object to 0,0
|
|
bpy.ops.object.location_clear()
|
|
bpy.ops.object.rotation_clear()
|
|
ori.select = False
|
|
for ob in o.objects:
|
|
activate(ob)
|
|
|
|
bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM')
|
|
|
|
# rot=ori.matrix_world.inverted()
|
|
# #rot.x=-rot.x
|
|
# #rot.y=-rot.y
|
|
# #rot.z=-rot.z
|
|
# rotationaxes = rotTo2axes(ori.rotation_euler,'CA')
|
|
#
|
|
# #bpy.context.space_data.pivot_point = 'CURSOR'
|
|
# #bpy.context.space_data.pivot_point = 'CURSOR'
|
|
#
|
|
# for ob in o.objects:
|
|
# ob.rotation_euler.rotate(rot)
|
|
|
|
|
|
def cleanupIndexed(operation):
|
|
s = bpy.context.scene
|
|
oriname = operation.name + 'orientation'
|
|
|
|
ori = s.objects[oriname]
|
|
path = s.objects["cam_path_{}{}".format(operation.name)]
|
|
|
|
ori.matrix_world = operation.orientation_matrix
|
|
# set correct path location
|
|
path.location = ori.location
|
|
path.rotation_euler = ori.rotation_euler
|
|
|
|
print(ori.matrix_world, operation.orientation_matrix)
|
|
for i, ob in enumerate(operation.objects): # TODO: fix this here wrong order can cause objects out of place
|
|
ob.parent = operation.parents[i]
|
|
for i, ob in enumerate(operation.objects):
|
|
ob.matrix_world = operation.matrices[i]
|
|
|
|
|
|
def rotTo2axes(e, axescombination):
|
|
"""converts an orientation object rotation to rotation defined by 2 rotational axes on the machine - for indexed machining.
|
|
attempting to do this for all axes combinations.
|
|
"""
|
|
v = Vector((0, 0, 1))
|
|
v.rotate(e)
|
|
# if axes
|
|
if axescombination == 'CA':
|
|
v2d = Vector((v.x, v.y))
|
|
a1base = Vector((0, -1)) # ?is this right?It should be vector defining 0 rotation
|
|
if v2d.length > 0:
|
|
cangle = a1base.angle_signed(v2d)
|
|
else:
|
|
return (0, 0)
|
|
v2d = Vector((v2d.length, v.z))
|
|
a2base = Vector((0, 1))
|
|
aangle = a2base.angle_signed(v2d)
|
|
print('angles', cangle, aangle)
|
|
return (cangle, aangle)
|
|
|
|
elif axescombination == 'CB':
|
|
v2d = Vector((v.x, v.y))
|
|
a1base = Vector((1, 0)) # ?is this right?It should be vector defining 0 rotation
|
|
if v2d.length > 0:
|
|
cangle = a1base.angle_signed(v2d)
|
|
else:
|
|
return (0, 0)
|
|
v2d = Vector((v2d.length, v.z))
|
|
a2base = Vector((0, 1))
|
|
|
|
bangle = a2base.angle_signed(v2d)
|
|
|
|
print('angles', cangle, bangle)
|
|
|
|
return (cangle, bangle)
|
|
|
|
# v2d=((v[a[0]],v[a[1]]))
|
|
# angle1=a1base.angle(v2d)#C for ca
|
|
# print(angle1)
|
|
# if axescombination[0]=='C':
|
|
# e1=Vector((0,0,-angle1))
|
|
# elif axescombination[0]=='A':#TODO: finish this after prototyping stage
|
|
# pass;
|
|
# v.rotate(e1)
|
|
# vbase=Vector(0,1,0)
|
|
# bangle=v.angle(vzbase)
|
|
# print(v)
|
|
# print(bangle)
|
|
|
|
return (angle1, angle2)
|
|
|
|
|
|
|
|
|
|
def reload_paths(o):
|
|
oname = "cam_path_" + o.name
|
|
s = bpy.context.scene
|
|
# for o in s.objects:
|
|
ob = None
|
|
old_pathmesh = None
|
|
if oname in s.objects:
|
|
old_pathmesh = s.objects[oname].data
|
|
ob = s.objects[oname]
|
|
|
|
picklepath = getCachePath(o) + '.pickle'
|
|
f = open(picklepath, 'rb')
|
|
d = pickle.load(f)
|
|
f.close()
|
|
|
|
# passed=False
|
|
# while not passed:
|
|
# try:
|
|
# f=open(picklepath,'rb')
|
|
# d=pickle.load(f)
|
|
# f.close()
|
|
# passed=True
|
|
# except:
|
|
# print('sleep')
|
|
# time.sleep(1)
|
|
|
|
o.warnings = d['warnings']
|
|
o.duration = d['duration']
|
|
verts = d['path']
|
|
|
|
edges = []
|
|
for a in range(0, len(verts) - 1):
|
|
edges.append((a, a + 1))
|
|
|
|
oname = "cam_path_" + o.name
|
|
mesh = bpy.data.meshes.new(oname)
|
|
mesh.name = oname
|
|
mesh.from_pydata(verts, edges, [])
|
|
|
|
if oname in s.objects:
|
|
s.objects[oname].data = mesh
|
|
else:
|
|
object_utils.object_data_add(bpy.context, mesh, operator=None)
|
|
ob = bpy.context.active_object
|
|
ob.name = oname
|
|
ob = s.objects[oname]
|
|
ob.location = (0, 0, 0)
|
|
o.path_object_name = oname
|
|
o.changed = False
|
|
|
|
if old_pathmesh != None:
|
|
bpy.data.meshes.remove(old_pathmesh)
|