Source code for cam.cam_chunk

"""BlenderCAM 'chunk.py' © 2012 Vilem Novak
 
Classes and Functions to build, store and optimize CAM path chunks.
"""

from math import (
    ceil,
    cos,
    hypot,
    pi,
    sin,
    sqrt,
    tan,
)

import numpy as np
from shapely.geometry import polygon as spolygon
from shapely import geometry as sgeometry

import bpy
from mathutils import Vector

from . import polygon_utils_cam
from .simple import (
    activate,
    dist2d,
    progress,
)
from .exception import CamException
from .numba_wrapper import jit


[docs] def Rotate_pbyp(originp, p, ang): # rotate point around another point with angle ox, oy, oz = originp px, py, oz = p if ang == abs(pi / 2): d = ang / abs(ang) qx = ox + d * (oy - py) qy = oy + d * (px - ox) else: qx = ox + cos(ang) * (px - ox) - sin(ang) * (py - oy) qy = oy + sin(ang) * (px - ox) + cos(ang) * (py - oy) rot_p = [qx, qy, oz] return rot_p
@jit(nopython=True, parallel=True, fastmath=True, cache=True)
[docs] def _internalXyDistanceTo(ourpoints, theirpoints, cutoff): v1 = ourpoints[0] v2 = theirpoints[0] minDistSq = (v1[0] - v2[0]) ** 2 + (v1[1] - v2[1]) ** 2 cutoffSq = cutoff**2 for v1 in ourpoints: for v2 in theirpoints: distSq = (v1[0] - v2[0]) ** 2 + (v1[1] - v2[1]) ** 2 if distSq < cutoffSq: return sqrt(distSq) minDistSq = min(distSq, minDistSq) return sqrt(minDistSq)
# for building points - stores points as lists for easy insert /append behaviour
[docs] class camPathChunkBuilder: def __init__(self, inpoints=None, startpoints=None, endpoints=None, rotations=None): if inpoints is None: inpoints = []
[docs] self.points = inpoints
[docs] self.startpoints = startpoints or []
[docs] self.endpoints = endpoints or []
[docs] self.rotations = rotations or []
[docs] self.depth = None
[docs] def to_chunk(self): chunk = camPathChunk( self.points, self.startpoints, self.endpoints, self.rotations ) if len(self.points) > 2 and np.array_equal(self.points[0], self.points[-1]): chunk.closed = True if self.depth is not None: chunk.depth = self.depth return chunk
# an actual chunk - stores points as numpy arrays
[docs] class camPathChunk: # parents=[] # children=[] # sorted=False # progressIndex=-1# for e.g. parallel strategy, when trying to save time.. def __init__(self, inpoints, startpoints=None, endpoints=None, rotations=None): # name this as _points so nothing external accesses it directly # for 3 axes, this is only storage of points. For N axes, here go the sampled points if len(inpoints) == 0: self.points = np.empty(shape=(0, 3)) else: self.points = np.array(inpoints)
[docs] self.poly = None # get polygon just in time
[docs] self.simppoly = None
if startpoints: # from where the sweep test begins, but also retract point for given path self.startpoints = startpoints else: self.startpoints = [] if endpoints: self.endpoints = endpoints else: self.endpoints = [] # where sweep test ends if rotations: self.rotations = rotations else: self.rotations = [] # rotation of the machine axes
[docs] self.closed = False
[docs] self.children = []
[docs] self.parents = []
# self.unsortedchildren=False
[docs] self.sorted = False # if the chunk has allready been milled in the simulation
[docs] self.length = 0 # this is total length of this chunk.
[docs] self.zstart = 0 # this is stored for ramps mainly,
# because they are added afterwards, but have to use layer info
[docs] self.zend = 0 #
[docs] def update_poly(self): if len(self.points) > 2: self.poly = sgeometry.Polygon(self.points[:, 0:2]) else: self.poly = sgeometry.Polygon()
[docs] def get_point(self, n): return self.points[n].tolist()
[docs] def get_points(self): return self.points.tolist()
[docs] def get_points_np(self): return self.points
[docs] def set_points(self, points): self.points = np.array(points)
[docs] def count(self): return len(self.points)
[docs] def copy(self): nchunk = camPathChunk( inpoints=self.points.copy(), startpoints=self.startpoints, endpoints=self.endpoints, rotations=self.rotations, ) nchunk.closed = self.closed nchunk.children = self.children nchunk.parents = self.parents nchunk.sorted = self.sorted nchunk.length = self.length return nchunk
[docs] def shift(self, x, y, z): self.points = self.points + np.array([x, y, z]) for i, p in enumerate(self.startpoints): self.startpoints[i] = (p[0] + x, p[1] + y, p[2] + z) for i, p in enumerate(self.endpoints): self.endpoints[i] = (p[0] + x, p[1] + y, p[2] + z)
[docs] def setZ(self, z, if_bigger=False): if if_bigger: self.points[:, 2] = z if z > self.points[:, 2] else self.points[:, 2] else: self.points[:, 2] = z
[docs] def offsetZ(self, z): self.points[:, 2] += z
[docs] def flipX(self, x_centre): self.points[:, 0] = x_centre - self.points[:, 0]
[docs] def isbelowZ(self, z): return np.any(self.points[:, 2] < z)
[docs] def clampZ(self, z): np.clip(self.points[:, 2], z, None, self.points[:, 2])
[docs] def clampmaxZ(self, z): np.clip(self.points[:, 2], None, z, self.points[:, 2])
[docs] def dist(self, pos, o): if self.closed: dist_sq = (pos[0] - self.points[:, 0]) ** 2 + ( pos[1] - self.points[:, 1] ) ** 2 return sqrt(np.min(dist_sq)) else: if o.movement.type == "MEANDER": d1 = dist2d(pos, self.points[0]) d2 = dist2d(pos, self.points[-1]) # if d2<d1: # ch.points.reverse() return min(d1, d2) else: return dist2d(pos, self.points[0])
[docs] def distStart(self, pos, o): return dist2d(pos, self.points[0])
[docs] def xyDistanceWithin(self, other, cutoff): if self.poly is None: self.update_poly() if other.poly is None: other.update_poly() if not self.poly.is_empty and not other.poly.is_empty: return self.poly.dwithin(other.poly, cutoff) else: return _internalXyDistanceTo(self.points, other.points, cutoff) < cutoff
# if cutoff is set, then the first distance < cutoff is returned
[docs] def xyDistanceTo(self, other, cutoff=0): if self.poly is None: self.update_poly() if other.poly is None: other.update_poly() if not self.poly.is_empty and not other.poly.is_empty: # both polygons have >2 points # simplify them if they aren't already, to speed up distance finding if self.simppoly is None: self.simppoly = self.poly.simplify(0.0003).boundary if other.simppoly is None: other.simppoly = other.poly.simplify(0.0003).boundary return self.simppoly.distance(other.simppoly) else: # this is the old method, preferably should be replaced in most cases except parallel # where this method works probably faster. # print('warning, sorting will be slow due to bad parenting in parentChildDist') return _internalXyDistanceTo(self.points, other.points, cutoff)
[docs] def adaptdist(self, pos, o): # reorders chunk so that it starts at the closest point to pos. if self.closed: dist_sq = (pos[0] - self.points[:, 0]) ** 2 + ( pos[1] - self.points[:, 1] ) ** 2 point_idx = np.argmin(dist_sq) new_points = np.concatenate( (self.points[point_idx:], self.points[: point_idx + 1]) ) self.points = new_points else: if o.movement.type == "MEANDER": d1 = dist2d(pos, self.points[0]) d2 = dist2d(pos, self.points[-1]) if d2 < d1: self.points = np.flip(self.points, axis=0)
[docs] def getNextClosest(self, o, pos): # finds closest chunk that can be milled, when inside sorting hierarchy. mind = 100000000000 self.cango = False closest = None testlist = [] testlist.extend(self.children) tested = [] tested.extend(self.children) ch = None while len(testlist) > 0: chtest = testlist.pop() if not chtest.sorted: self.cango = False cango = True for child in chtest.children: if not child.sorted: if child not in tested: testlist.append(child) tested.append(child) cango = False if cango: d = chtest.dist(pos, o) if d < mind: ch = chtest mind = d if ch is not None: # print('found some') return ch # print('returning none') return None
[docs] def getLength(self): # computes length of the chunk - in 3d point_differences = self.points[0:-1, :] - self.points[1:, :] distances = np.linalg.norm(point_differences, axis=1) self.length = np.sum(distances)
[docs] def reverse(self): self.points = np.flip(self.points, axis=0) self.startpoints.reverse() self.endpoints.reverse() self.rotations.reverse()
[docs] def pop(self, index): print("WARNING: Popping from Chunk Is Slow", self, index) self.points = np.concatenate( (self.points[0:index], self.points[index + 1:]), axis=0 ) if len(self.startpoints) > 0: self.startpoints.pop(index) self.endpoints.pop(index) self.rotations.pop(index)
[docs] def dedupePoints(self): if len(self.points) > 1: keep_points = np.empty(self.points.shape[0], dtype=bool) keep_points[0] = True diff_points = np.sum((self.points[1:] - self.points[:1]) ** 2, axis=1) keep_points[1:] = diff_points > 0.000000001 self.points = self.points[keep_points, :]
[docs] def insert(self, at_index, point, startpoint=None, endpoint=None, rotation=None): self.append( point, startpoint=startpoint, endpoint=endpoint, rotation=rotation, at_index=at_index, )
[docs] def append( self, point, startpoint=None, endpoint=None, rotation=None, at_index=None ): if at_index is None: self.points = np.concatenate((self.points, np.array([point]))) if startpoint is not None: self.startpoints.append(startpoint) if endpoint is not None: self.endpoints.append(endpoint) if rotation is not None: self.rotations.append(rotation) else: self.points = np.concatenate( (self.points[0:at_index], np.array([point]), self.points[at_index:]) ) if startpoint is not None: self.startpoints[at_index:at_index] = [startpoint] if endpoint is not None: self.endpoints[at_index:at_index] = [endpoint] if rotation is not None: self.rotations[at_index:at_index] = [rotation]
[docs] def extend( self, points, startpoints=None, endpoints=None, rotations=None, at_index=None ): if len(points) == 0: return if at_index is None: self.points = np.concatenate((self.points, np.array(points))) if startpoints is not None: self.startpoints.extend(startpoints) if endpoints is not None: self.endpoints.extend(endpoints) if rotations is not None: self.rotations.extend(rotations) else: self.points = np.concatenate( (self.points[0:at_index], np.array(points), self.points[at_index:]) ) if startpoints is not None: self.startpoints[at_index:at_index] = startpoints if endpoints is not None: self.endpoints[at_index:at_index] = endpoints if rotations is not None: self.rotations[at_index:at_index] = rotations
[docs] def clip_points(self, minx, maxx, miny, maxy): """Remove Any Points Outside This Range""" included_values = (self.points[:, 0] >= minx) and ( (self.points[:, 0] <= maxx) and (self.points[:, 1] >= maxy) and (self.points[:, 1] <= maxy) ) self.points = self.points[included_values]
[docs] def rampContour(self, zstart, zend, o): stepdown = zstart - zend chunk_points = [] estlength = (zstart - zend) / tan(o.movement.ramp_in_angle) self.getLength() ramplength = estlength # min(ch.length,estlength) ltraveled = 0 endpoint = None i = 0 # z=zstart znew = 10 rounds = 0 # for counting if ramping makes more layers while endpoint is None and not (znew == zend and i == 0): # # for i,s in enumerate(ch.points): # print(i, znew, zend, len(ch.points)) s = self.points[i] if i > 0: s2 = self.points[i - 1] ltraveled += dist2d(s, s2) ratio = ltraveled / ramplength elif rounds > 0 and i == 0: s2 = self.points[-1] ltraveled += dist2d(s, s2) ratio = ltraveled / ramplength else: ratio = 0 znew = zstart - stepdown * ratio if znew <= zend: ratio = (z - zend) / (z - znew) v1 = Vector(chunk_points[-1]) v2 = Vector((s[0], s[1], znew)) v = v1 + ratio * (v2 - v1) chunk_points.append((v.x, v.y, max(s[2], v.z))) if zend == o.min.z and endpoint is None and self.closed: endpoint = i + 1 if endpoint == len(self.points): endpoint = 0 # print(endpoint,len(ch.points)) # else: znew = max(znew, zend, s[2]) chunk_points.append((s[0], s[1], znew)) z = znew if endpoint is not None: break i += 1 if i >= len(self.points): i = 0 rounds += 1 # if not o.use_layers: # endpoint=0 if endpoint is not None: # append final contour on the bottom z level i = endpoint started = False # print('finaliz') if i == len(self.points): i = 0 while i != endpoint or not started: started = True s = self.points[i] chunk_points.append((s[0], s[1], s[2])) # print(i,endpoint) i += 1 if i == len(self.points): i = 0 # ramp out if o.movement.ramp_out and ( not o.use_layers or not o.first_down or (o.first_down and endpoint is not None) ): z = zend # i=endpoint while z < o.maxz: if i == len(self.points): i = 0 s1 = self.points[i] i2 = i - 1 if i2 < 0: i2 = len(self.points) - 1 s2 = self.points[i2] l = dist2d(s1, s2) znew = z + tan(o.movement.ramp_out_angle) * l if znew > o.maxz: ratio = (z - o.maxz) / (z - znew) v1 = Vector(chunk_points[-1]) v2 = Vector((s1[0], s1[1], znew)) v = v1 + ratio * (v2 - v1) chunk_points.append((v.x, v.y, v.z)) else: chunk_points.append((s1[0], s1[1], znew)) z = znew i += 1 # TODO: convert to numpy properly self.points = np.array(chunk_points)
[docs] def rampZigZag(self, zstart, zend, o): # TODO: convert to numpy properly if zend == None: zend = self.points[0][2] chunk_points = [] # print(zstart,zend) if zend < zstart: # this check here is only for stupid setup, # when the chunks lie actually above operation start z. stepdown = zstart - zend estlength = (zstart - zend) / tan(o.movement.ramp_in_angle) self.getLength() if self.length > 0: # for single point chunks.. ramplength = estlength zigzaglength = ramplength / 2.000 turns = 1 print("turns %i" % turns) if zigzaglength > self.length: turns = ceil(zigzaglength / self.length) ramplength = turns * self.length * 2.0 zigzaglength = self.length ramppoints = self.points.tolist() else: zigzagtraveled = 0.0 haspoints = False ramppoints = [ (self.points[0][0], self.points[0][1], self.points[0][2]) ] i = 1 while not haspoints: # print(i,zigzaglength,zigzagtraveled) p1 = ramppoints[-1] p2 = self.points[i] d = dist2d(p1, p2) zigzagtraveled += d if zigzagtraveled >= zigzaglength or i + 1 == len(self.points): ratio = 1 - (zigzagtraveled - zigzaglength) / d if i + 1 == len( self.points ): # this condition is for a rare case of combined layers+bridges+ramps.. ratio = 1 v = p1 + ratio * (p2 - p1) ramppoints.append(v.tolist()) haspoints = True else: ramppoints.append(p2) i += 1 negramppoints = ramppoints.copy() negramppoints.reverse() ramppoints.extend(negramppoints[1:]) traveled = 0.0 chunk_points.append( ( self.points[0][0], self.points[0][1], max(self.points[0][2], zstart), ) ) for r in range(turns): for p in range(0, len(ramppoints)): p1 = chunk_points[-1] p2 = ramppoints[p] d = dist2d(p1, p2) traveled += d ratio = traveled / ramplength znew = zstart - stepdown * ratio # max value here is so that it doesn't go chunk_points.append((p2[0], p2[1], max(p2[2], znew))) # below surface in the case of 3d paths # chunks = setChunksZ([ch],zend) chunk_points.extend(self.points.tolist()) ###################################### # ramp out - this is the same thing, just on the other side.. if o.movement.ramp_out: zstart = o.maxz zend = self.points[-1][2] # again, sometimes a chunk could theoretically end above the starting level. if zend < zstart: stepdown = zstart - zend estlength = (zstart - zend) / tan(o.movement.ramp_out_angle) self.getLength() if self.length > 0: ramplength = estlength zigzaglength = ramplength / 2.000 turns = 1 print("turns %i" % turns) if zigzaglength > self.length: turns = ceil(zigzaglength / self.length) ramplength = turns * self.length * 2.0 zigzaglength = self.length ramppoints = self.points.tolist() # revert points here, we go the other way. ramppoints.reverse() else: zigzagtraveled = 0.0 haspoints = False ramppoints = [ ( self.points[-1][0], self.points[-1][1], self.points[-1][2], ) ] i = len(self.points) - 2 while not haspoints: # print(i,zigzaglength,zigzagtraveled) p1 = ramppoints[-1] p2 = self.points[i] d = dist2d(p1, p2) zigzagtraveled += d if zigzagtraveled >= zigzaglength or i + 1 == len( self.points ): ratio = 1 - (zigzagtraveled - zigzaglength) / d if i + 1 == len( self.points ): # this condition is for a rare case of # combined layers+bridges+ramps... ratio = 1 # print((ratio,zigzaglength)) v = p1 + ratio * (p2 - p1) ramppoints.append(v.tolist()) haspoints = True # elif : else: ramppoints.append(p2) i -= 1 negramppoints = ramppoints.copy() negramppoints.reverse() ramppoints.extend(negramppoints[1:]) traveled = 0.0 for r in range(turns): for p in range(0, len(ramppoints)): p1 = chunk_points[-1] p2 = ramppoints[p] d = dist2d(p1, p2) traveled += d ratio = 1 - (traveled / ramplength) znew = zstart - stepdown * ratio chunk_points.append((p2[0], p2[1], max(p2[2], znew))) # max value here is so that it doesn't go below surface in the case of 3d paths self.points = np.array(chunk_points)
# modify existing path start point
[docs] def changePathStart(self, o): if o.profile_start > 0: newstart = o.profile_start chunkamt = len(self.points) newstart = newstart % chunkamt self.points = np.concatenate( (self.points[newstart:], self.points[:newstart]) )
[docs] def breakPathForLeadinLeadout(self, o): iradius = o.lead_in oradius = o.lead_out if iradius + oradius > 0: chunkamt = len(self.points) for i in range(chunkamt - 1): apoint = self.points[i] bpoint = self.points[i + 1] bmax = bpoint[0] - apoint[0] bmay = bpoint[1] - apoint[1] segmentLength = hypot(bmax, bmay) # find segment length if segmentLength > 2 * max( iradius, oradius ): # Be certain there is enough room for the leadin and leadiout # add point on the line here # average of the two x points to find center newpointx = (bpoint[0] + apoint[0]) / 2 # average of the two y points to find center newpointy = (bpoint[1] + apoint[1]) / 2 self.points = np.concatenate( ( self.points[: i + 1], np.array([[newpointx, newpointy, apoint[2]]]), self.points[i + 1:], ) )
[docs] def leadContour(self, o): perimeterDirection = 1 # 1 is clockwise, 0 is CCW if o.movement.spindle_rotation == "CW": if o.movement.type == "CONVENTIONAL": perimeterDirection = 0 if self.parents: # if it is inside another parent perimeterDirection ^= 1 # toggle with a bitwise XOR print("Has Parent") if perimeterDirection == 1: print("Path Direction Is Clockwise") else: print("Path Direction Is Counter Clockwise") iradius = o.lead_in oradius = o.lead_out start = self.points[0] nextp = self.points[1] rpoint = Rotate_pbyp(start, nextp, pi / 2) dx = rpoint[0] - start[0] dy = rpoint[1] - start[1] la = hypot(dx, dy) pvx = (iradius * dx) / la + start[0] # arc center(x) pvy = (iradius * dy) / la + start[1] # arc center(y) arc_c = [pvx, pvy, start[2]] # TODO: this could easily be numpy chunk_points = [] # create a new cutting path # add lead in arc in the begining if round(o.lead_in, 6) > 0.0: for i in range(15): iangle = -i * (pi / 2) / 15 arc_p = Rotate_pbyp(arc_c, start, iangle) chunk_points.insert(0, arc_p) # glue rest of the path to the arc chunk_points.extend(self.points.tolist()) # for i in range(len(self.points)): # chunk_points.append(self.points[i]) # add lead out arc to the end if round(o.lead_in, 6) > 0.0: for i in range(15): iangle = i * (pi / 2) / 15 arc_p = Rotate_pbyp(arc_c, start, iangle) chunk_points.append(arc_p) self.points = np.array(chunk_points)
[docs] def chunksCoherency(chunks): # checks chunks for their stability, for pencil path. # it checks if the vectors direction doesn't jump too much too quickly, # if this happens it splits the chunk on such places, # too much jumps = deletion of the chunk. this is because otherwise the router has to slow down too often, # but also means that some parts detected by cavity algorithm won't be milled nchunks = [] for chunk in chunks: if len(chunk.points) > 2: nchunk = camPathChunkBuilder() # doesn't check for 1 point chunks here, they shouldn't get here at all. lastvec = Vector(chunk.points[1]) - Vector(chunk.points[0]) for i in range(0, len(chunk.points) - 1): nchunk.points.append(chunk.points[i]) vec = Vector(chunk.points[i + 1]) - Vector(chunk.points[i]) angle = vec.angle(lastvec, vec) # print(angle,i) if angle > 1.07: # 60 degrees is maximum toleration for pencil paths. if len(nchunk.points) > 4: # this is a testing threshold nchunks.append(nchunk.to_chunk()) nchunk = camPathChunkBuilder() lastvec = vec if len(nchunk.points) > 4: # this is a testing threshold nchunk.points = np.array(nchunk.points) nchunks.append(nchunk) return nchunks
[docs] def setChunksZ(chunks, z): newchunks = [] for ch in chunks: chunk = ch.copy() chunk.setZ(z) newchunks.append(chunk) return newchunks
# don't make this @jit parallel, because it sometimes gets called with small N # and the overhead of threading is too much. @jit(nopython=True, fastmath=True, cache=True)
[docs] def _optimize_internal( points, keep_points, e, protect_vertical, protect_vertical_limit ): # inlined so that numba can optimize it nicely def _mag_sq(v1): return v1[0] ** 2 + v1[1] ** 2 + v1[2] ** 2 def _dot_pr(v1, v2): return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2] def _applyVerticalLimit(v1, v2, cos_limit): """Test Path Segment on Verticality Threshold, for Protect_vertical Option""" z = abs(v1[2] - v2[2]) if z > 0: # don't use this vector because dot product of 0,0,1 is trivially just v2[2] # vec_up = np.array([0, 0, 1]) vec_diff = v1 - v2 vec_diff2 = v2 - v1 vec_diff_mag = np.sqrt(_mag_sq(vec_diff)) # dot product = cos(angle) * mag1 * mag2 cos1_times_mag = vec_diff[2] cos2_times_mag = vec_diff2[2] if cos1_times_mag > cos_limit * vec_diff_mag: # vertical, moving down v1[0] = v2[0] v1[1] = v2[1] elif cos2_times_mag > cos_limit * vec_diff_mag: # vertical, moving up v2[0] = v1[0] v2[1] = v1[1] cos_limit = cos(protect_vertical_limit) prev_i = 0 for i in range(1, points.shape[0] - 1): v1 = points[prev_i] v2 = points[i + 1] vmiddle = points[i] line_direction = v2 - v1 line_length = sqrt(_mag_sq(line_direction)) if line_length == 0: # don't keep duplicate points keep_points[i] = False continue # normalize line direction line_direction *= 1.0 / line_length # N in formula below # X = A + tN (line formula) Distance to point P # A = v1, N = line_direction, P = vmiddle # distance = || (P - A) - ((P-A).N)N || point_offset = vmiddle - v1 distance_sq = _mag_sq( point_offset - (line_direction * _dot_pr(point_offset, line_direction)) ) # compare on squared distance to save a sqrt if distance_sq < e * e: keep_points[i] = False else: keep_points[i] = True if protect_vertical: _applyVerticalLimit(points[prev_i], points[i], cos_limit) prev_i = i
[docs] def optimizeChunk(chunk, operation): if len(chunk.points) > 2: points = chunk.points naxispoints = False if len(chunk.startpoints) > 0: startpoints = chunk.startpoints endpoints = chunk.endpoints naxispoints = True protect_vertical = ( operation.movement.protect_vertical and operation.machine_axes == "3" ) keep_points = np.full(points.shape[0], True) # shape points need to be on line, # but we need to protect vertical - which # means changing point values # bits of this are moved from simple.py so that # numba can optimize as a whole _optimize_internal( points, keep_points, operation.optimisation.optimize_threshold * 0.000001, protect_vertical, operation.movement.protect_vertical_limit, ) # now do numpy select by boolean array chunk.points = points[keep_points] if naxispoints: # list comprehension so we don't have to do tons of appends chunk.startpoints = [ chunk.startpoints[i] for i, b in enumerate(keep_points) if b == True ] chunk.endpoints = [ chunk.endpoints[i] for i, b in enumerate(keep_points) if b == True ] chunk.rotations = [ chunk.rotations[i] for i, b in enumerate(keep_points) if b == True ] return chunk
[docs] def limitChunks( chunks, o, force=False ): # TODO: this should at least add point on area border... # but shouldn't be needed at all at the first place... if o.use_limit_curve or force: nchunks = [] for ch in chunks: prevsampled = True nch = camPathChunkBuilder() nch1 = None closed = True for s in ch.points: sampled = o.ambient.contains(sgeometry.Point(s[0], s[1])) if not sampled and len(nch.points) > 0: nch.closed = False closed = False nchunks.append(nch.to_chunk()) if nch1 is None: nch1 = nchunks[-1] nch = camPathChunkBuilder() elif sampled: nch.points.append(s) prevsampled = sampled if ( len(nch.points) > 2 and closed and ch.closed and np.array_equal(ch.points[0], ch.points[-1]) ): nch.closed = True elif ( ch.closed and nch1 is not None and len(nch.points) > 1 and np.array_equal(nch.points[-1], nch1.points[0]) ): # here adds beginning of closed chunk to the end, if the chunks were split during limiting nch.points.extend(nch1.points.tolist()) nchunks.remove(nch1) print("joining stuff") if len(nch.points) > 0: nchunks.append(nch.to_chunk()) return nchunks else: return chunks
[docs] def parentChildPoly(parents, children, o): # hierarchy based on polygons - a polygon inside another is his child. # hierarchy works like this: - children get milled first. for parent in parents: if parent.poly is None: parent.update_poly() for child in children: if child.poly is None: child.update_poly() if child != parent: # and len(child.poly)>0 if parent.poly.contains(sgeometry.Point(child.poly.boundary.coords[0])): parent.children.append(child) child.parents.append(parent)
[docs] def parentChildDist(parents, children, o, distance=None): # parenting based on x,y distance between chunks # hierarchy works like this: - children get milled first. if distance is None: dlim = o.dist_between_paths * 2 if ( o.strategy == "PARALLEL" or o.strategy == "CROSS" ) and o.movement.parallel_step_back: dlim = dlim * 2 else: dlim = distance for child in children: for parent in parents: isrelation = False if parent != child: if parent.xyDistanceWithin(child, cutoff=dlim): parent.children.append(child) child.parents.append(parent)
[docs] def parentChild(parents, children, o): # connect all children to all parents. Useful for any type of defining hierarchy. # hierarchy works like this: - children get milled first. for child in children: for parent in parents: if parent != child: parent.children.append(child) child.parents.append(parent)
# this does more cleve chunks to Poly with hierarchies... ;)
[docs] def chunksToShapely(chunks): # print ('analyzing paths') for ch in chunks: # first convert chunk to poly if len(ch.points) > 2: # pchunk=[] ch.poly = sgeometry.Polygon(ch.points[:, 0:2]) if not ch.poly.is_valid: ch.poly = sgeometry.Polygon() else: ch.poly = sgeometry.Polygon() for ppart in chunks: # then add hierarchy relations for ptest in chunks: if ppart != ptest: if not ppart.poly.is_empty and not ptest.poly.is_empty: if ptest.poly.contains(ppart.poly): # hierarchy works like this: - children get milled first. ppart.parents.append(ptest) for ( ch ) in ( chunks ): # now make only simple polygons with holes, not more polys inside others found = False if len(ch.parents) % 2 == 1: for parent in ch.parents: if len(parent.parents) + 1 == len(ch.parents): # nparents serves as temporary storage for parents, ch.nparents = [parent] # not to get mixed with the first parenting during the check found = True break if not found: ch.nparents = [] for ch in chunks: # then subtract the 1st level holes ch.parents = ch.nparents ch.nparents = None if len(ch.parents) > 0: try: ch.parents[0].poly = ch.parents[0].poly.difference( ch.poly ) # sgeometry.Polygon( ch.parents[0].poly, ch.poly) except: print("chunksToShapely oops!") lastPt = None tolerance = 0.0000003 newPoints = [] for pt in ch.points: toleranceXok = True toleranceYok = True if lastPt is not None: if abs(pt[0] - lastPt[0]) < tolerance: toleranceXok = False if abs(pt[1] - lastPt[1]) < tolerance: toleranceYok = False if toleranceXok or toleranceYok: newPoints.append(pt) lastPt = pt else: newPoints.append(pt) lastPt = pt toleranceXok = True toleranceYok = True if abs(newPoints[0][0] - lastPt[0]) < tolerance: toleranceXok = False if abs(newPoints[0][1] - lastPt[1]) < tolerance: toleranceYok = False if not toleranceXok and not toleranceYok: newPoints.pop() ch.points = np.array(newPoints) ch.poly = sgeometry.Polygon(ch.points) try: ch.parents[0].poly = ch.parents[0].poly.difference(ch.poly) except: # print('chunksToShapely double oops!') lastPt = None tolerance = 0.0000003 newPoints = [] for pt in ch.parents[0].points: toleranceXok = True toleranceYok = True # print( '{0:.9f}, {0:.9f}, {0:.9f}'.format(pt[0], pt[1], pt[2]) ) # print(pt) if lastPt is not None: if abs(pt[0] - lastPt[0]) < tolerance: toleranceXok = False if abs(pt[1] - lastPt[1]) < tolerance: toleranceYok = False if toleranceXok or toleranceYok: newPoints.append(pt) lastPt = pt else: newPoints.append(pt) lastPt = pt toleranceXok = True toleranceYok = True if abs(newPoints[0][0] - lastPt[0]) < tolerance: toleranceXok = False if abs(newPoints[0][1] - lastPt[1]) < tolerance: toleranceYok = False if not toleranceXok and not toleranceYok: newPoints.pop() # print('starting and ending points too close, removing ending point') ch.parents[0].points = np.array(newPoints) ch.parents[0].poly = sgeometry.Polygon(ch.parents[0].points) ch.parents[0].poly = ch.parents[0].poly.difference( ch.poly ) # sgeometry.Polygon( ch.parents[0].poly, ch.poly) returnpolys = [] for polyi in range(0, len(chunks)): # export only the booleaned polygons ch = chunks[polyi] if not ch.poly.is_empty: if len(ch.parents) == 0: returnpolys.append(ch.poly) from shapely.geometry import MultiPolygon polys = MultiPolygon(returnpolys) return polys
[docs] def meshFromCurveToChunk(object): mesh = object.data # print('detecting contours from curve') chunks = [] chunk = camPathChunkBuilder() ek = mesh.edge_keys d = {} for e in ek: d[e] = 1 # dk = d.keys() x = object.location.x y = object.location.y z = object.location.z lastvi = 0 vtotal = len(mesh.vertices) perc = 0 progress("Processing Curve - START - Vertices: " + str(vtotal)) for vi in range(0, len(mesh.vertices) - 1): co = (mesh.vertices[vi].co + object.location).to_tuple() if not dk.isdisjoint([(vi, vi + 1)]) and d[(vi, vi + 1)] == 1: chunk.points.append(co) else: chunk.points.append(co) if len(chunk.points) > 2 and ( not (dk.isdisjoint([(vi, lastvi)])) or not (dk.isdisjoint([(lastvi, vi)])) ): # this was looping chunks of length of only 2 points... # print('itis') chunk.closed = True chunk.points.append( (mesh.vertices[lastvi].co + object.location).to_tuple() ) # add first point to end#originally the z was mesh.vertices[lastvi].co.z+z lastvi = vi + 1 chunk = chunk.to_chunk() chunk.dedupePoints() if chunk.count() >= 1: # dump single point chunks chunks.append(chunk) chunk = camPathChunkBuilder() progress("Processing Curve - FINISHED") vi = len(mesh.vertices) - 1 chunk.points.append( ( mesh.vertices[vi].co.x + x, mesh.vertices[vi].co.y + y, mesh.vertices[vi].co.z + z, ) ) if not (dk.isdisjoint([(vi, lastvi)])) or not (dk.isdisjoint([(lastvi, vi)])): chunk.closed = True chunk.points.append( ( mesh.vertices[lastvi].co.x + x, mesh.vertices[lastvi].co.y + y, mesh.vertices[lastvi].co.z + z, ) ) chunk = chunk.to_chunk() chunk.dedupePoints() if chunk.count() >= 1: # dump single point chunks chunks.append(chunk) return chunks
[docs] def makeVisible(o): storage = [True, []] if not o.visible_get(): storage[0] = False cam_collection = bpy.data.collections.new("cam") bpy.context.scene.collection.children.link(cam_collection) cam_collection.objects.link(bpy.context.object) for i in range(0, 20): storage[1].append(o.layers[i]) o.layers[i] = bpy.context.scene.layers[i] return storage
[docs] def restoreVisibility(o, storage): o.hide_viewport = storage[0] # print(storage) for i in range(0, 20): o.layers[i] = storage[1][i]
[docs] def meshFromCurve(o, use_modifiers=False): activate(o) bpy.ops.object.duplicate() bpy.ops.object.parent_clear(type="CLEAR_KEEP_TRANSFORM") co = bpy.context.active_object # support for text objects is only and only here, just convert them to curves. if co.type == "FONT": bpy.ops.object.convert(target="CURVE", keep_original=False) elif co.type != "CURVE": # curve must be a curve... bpy.ops.object.delete() # delete temporary object raise CamException("Source Curve Object Must Be of Type Curve") co.data.dimensions = "3D" co.data.bevel_depth = 0 co.data.extrude = 0 # first, convert to mesh to avoid parenting issues with hooks, then apply locrotscale. bpy.ops.object.convert(target="MESH", keep_original=False) if use_modifiers: eval_object = co.evaluated_get(bpy.context.evaluated_depsgraph_get()) newmesh = bpy.data.meshes.new_from_object(eval_object) oldmesh = co.data co.modifiers.clear() co.data = newmesh bpy.data.meshes.remove(oldmesh) try: bpy.ops.object.transform_apply(location=True, rotation=False, scale=False) bpy.ops.object.transform_apply(location=False, rotation=True, scale=False) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) except: pass return bpy.context.active_object
[docs] def curveToChunks(o, use_modifiers=False): co = meshFromCurve(o, use_modifiers) chunks = meshFromCurveToChunk(co) co = bpy.context.active_object bpy.ops.object.select_all(action="DESELECT") bpy.data.objects[co.name].select_set(True) bpy.ops.object.delete() return chunks
[docs] def shapelyToChunks(p, zlevel): # chunk_builders = [] # p=sortContours(p) seq = polygon_utils_cam.shapelyToCoords(p) i = 0 for s in seq: # progress(p[i]) if len(s) > 1: chunk = camPathChunkBuilder([]) for v in s: if p.has_z: chunk.points.append((v[0], v[1], v[2])) else: chunk.points.append((v[0], v[1], zlevel)) chunk_builders.append(chunk) i += 1 chunk_builders.reverse() # this is for smaller shapes first. return [c.to_chunk() for c in chunk_builders]
[docs] def chunkToShapely(chunk): p = spolygon.Polygon(chunk.points) return p
[docs] def chunksRefine(chunks, o): """Add Extra Points in Between for Chunks""" for ch in chunks: # print('before',len(ch)) newchunk = [] v2 = Vector(ch.points[0]) # print(ch.points) for s in ch.points: v1 = Vector(s) v = v1 - v2 if v.length > o.dist_along_paths: d = v.length v.normalize() i = 0 vref = Vector((0, 0, 0)) while vref.length < d: i += 1 vref = v * o.dist_along_paths * i if vref.length < d: p = v2 + vref newchunk.append((p.x, p.y, p.z)) newchunk.append(s) v2 = v1 ch.points = np.array(newchunk) return chunks
[docs] def chunksRefineThreshold(chunks, distance, limitdistance): """Add Extra Points in Between for Chunks. for Medial Axis Strategy only!""" for ch in chunks: newchunk = [] v2 = Vector(ch.points[0]) for s in ch.points: v1 = Vector(s) v = v1 - v2 if v.length > limitdistance: d = v.length v.normalize() i = 1 vref = Vector((0, 0, 0)) while vref.length < d / 2: vref = v * distance * i if vref.length < d: p = v2 + vref newchunk.append((p.x, p.y, p.z)) i += 1 # because of the condition, so it doesn't run again. vref = v * distance * i while i > 0: vref = v * distance * i if vref.length < d: p = v1 - vref newchunk.append((p.x, p.y, p.z)) i -= 1 newchunk.append(s) v2 = v1 ch.points = np.array(newchunk) return chunks