Source code for fabex.utilities.stroke_utils

from math import (
    ceil,
    pi,
)
import random

import numpy
import numpy as np

from mathutils import Vector, Euler


from .chunk_builder import (
    CamPathChunk,
    CamPathChunkBuilder,
)
from .geom_utils import get_circle_binary
from .image_utils import (
    prepare_area,
)
from .logging_utils import log
from .parent_utils import (
    parent_child_distance,
)


[docs] def crazy_stroke_image(o): """Generate a toolpath for a milling operation using a crazy stroke strategy. This function computes a path for a milling cutter based on the provided parameters and the offset image. It utilizes a circular cutter representation and evaluates potential cutting positions based on various thresholds. The algorithm iteratively tests different angles and lengths for the cutter's movement until the desired cutting area is achieved or the maximum number of tests is reached. Args: o (object): An object containing parameters such as cutter diameter, optimization settings, movement type, and thresholds for determining cutting effectiveness. Returns: list: A list of chunks representing the computed toolpath for the milling operation. """ # this surprisingly works, and can be used as a basis for something similar to adaptive milling strategy. minx, miny, minz, maxx, maxy, maxz = o.min.x, o.min.y, o.min.z, o.max.x, o.max.y, o.max.z r = int((o.cutter_diameter / 2.0) / o.optimisation.pixsize) d = 2 * r coef = 0.75 ar = o.offset_image.copy() maxarx = ar.shape[0] maxary = ar.shape[1] cutterArray = get_circle_binary(r) cutterArrayNegative = -cutterArray cutterimagepix = cutterArray.sum() # a threshold which says if it is valuable to cut in a direction satisfypix = cutterimagepix * o.crazy_threshold_1 toomuchpix = cutterimagepix * o.crazy_threshold_2 indices = ar.nonzero() # first get white pixels startpix = ar.sum() # totpix = startpix chunk_builders = [] xs = indices[0][0] - r if xs < r: xs = r ys = indices[1][0] - r if ys < r: ys = r nchunk = CamPathChunkBuilder([(xs, ys)]) # startposition log.info(indices) log.info(f"{indices[0][0]}, {indices[1][0]}") # vector is 3d, blender somehow doesn't rotate 2d vectors with angles. lastvect = Vector((r, 0, 0)) # multiply *2 not to get values <1 pixel testvect = lastvect.normalized() * r / 2.0 rot = Euler((0, 0, 1)) i = 0 perc = 0 itests = 0 totaltests = 0 maxtests = 500 maxtotaltests = 1000000 log.info(f"{xs}, {ys}, {indices[0][0]}, {indices[1][0]}, {r}") ar[xs - r : xs - r + d, ys - r : ys - r + d] = ( ar[xs - r : xs - r + d, ys - r : ys - r + d] * cutterArrayNegative ) # range for angle of toolpath vector versus material vector anglerange = [-pi, pi] testangleinit = 0 angleincrement = 0.05 if (o.movement.type == "CLIMB" and o.movement.spindle_rotation == "CCW") or ( o.movement.type == "CONVENTIONAL" and o.movement.spindle_rotation == "CW" ): anglerange = [-pi, 0] testangleinit = 1 angleincrement = -angleincrement elif (o.movement.type == "CONVENTIONAL" and o.movement.spindle_rotation == "CCW") or ( o.movement.type == "CLIMB" and o.movement.spindle_rotation == "CW" ): anglerange = [0, pi] testangleinit = -1 angleincrement = angleincrement while totpix > 0 and totaltests < maxtotaltests: # a ratio when the algorithm is allowed to end success = False # define a vector which gets varied throughout the testing, growing and growing angle to sides. testangle = testangleinit testleftright = False testlength = r while not success: xs = nchunk.points[-1][0] + int(testvect.x) ys = nchunk.points[-1][1] + int(testvect.y) if xs > r + 1 and xs < ar.shape[0] - r - 1 and ys > r + 1 and ys < ar.shape[1] - r - 1: testar = ar[xs - r : xs - r + d, ys - r : ys - r + d] * cutterArray if 0: log.info("Test") log.info(f"{testar.sum()}, {satisfypix}") log.info(f"{xs}, {ys}, {testlength}, {testangle}") log.info(lastvect) log.info(testvect) log.info(totpix) eatpix = testar.sum() cindices = testar.nonzero() cx = cindices[0].sum() / eatpix cy = cindices[1].sum() / eatpix v = Vector((cx - r, cy - r)) angle = testvect.to_2d().angle_signed(v) # this could be righthanded milling? lets see :) if anglerange[0] < angle < anglerange[1]: if toomuchpix > eatpix > satisfypix: success = True if success: nchunk.points.append([xs, ys]) lastvect = testvect ar[xs - r : xs - r + d, ys - r : ys - r + d] = ar[ xs - r : xs - r + d, ys - r : ys - r + d ] * (-cutterArray) totpix -= eatpix itests = 0 if 0: log.info("Success") log.info(f"{xs}, {ys}, {testlength}, {testangle}") log.info(lastvect) log.info(testvect) log.info(itests) else: # TODO: after all angles were tested into material higher than toomuchpix, it should cancel, # otherwise there is no problem with long travel in free space..... # TODO:the testing should start not from the same angle as lastvector, but more towards material. # So values closer to toomuchpix are obtained rather than satisfypix testvect = lastvect.normalized() * testlength right = True if testangleinit == 0: # meander if testleftright: testangle = -testangle testleftright = False else: testangle = abs(testangle) + angleincrement # increment angle testleftright = True else: # climb/conv. testangle += angleincrement if abs(testangle) > o.crazy_threshold_3: # /testlength testangle = testangleinit testlength += r / 4.0 if nchunk.points[-1][0] + testvect.x < r: testvect.x = r if nchunk.points[-1][1] + testvect.y < r: testvect.y = r if nchunk.points[-1][0] + testvect.x > maxarx - r: testvect.x = maxarx - r if nchunk.points[-1][1] + testvect.y > maxary - r: testvect.y = maxary - r rot.z = testangle testvect.rotate(rot) itests += 1 totaltests += 1 if itests > maxtests or testlength > r * 1.5: indices = ar.nonzero() chunk_builders.append(nchunk) if len(indices[0]) > 0: xs = indices[0][0] - r if xs < r: xs = r ys = indices[1][0] - r if ys < r: ys = r nchunk = CamPathChunkBuilder([(xs, ys)]) # startposition ar[xs - r : xs - r + d, ys - r : ys - r + d] = ( ar[xs - r : xs - r + d, ys - r : ys - r + d] * cutterArrayNegative ) r = random.random() * 2 * pi e = Euler((0, 0, r)) testvect = lastvect.normalized() * 4 # multiply *2 not to get values <1 pixel testvect.rotate(e) lastvect = testvect.copy() success = True itests = 0 i += 1 if i % 100 == 0: log.info("100 Succesfull Tests Done") totpix = ar.sum() log.info(totpix) log.info(totaltests) i = 0 chunk_builders.append(nchunk) for ch in chunk_builders: ch = ch.points for i in range(0, len(ch)): ch[i] = ( (ch[i][0] + coef - o.borderwidth) * o.optimisation.pixsize + minx, (ch[i][1] + coef - o.borderwidth) * o.optimisation.pixsize + miny, 0, ) return [c.to_chunk() for c in chunk_builders]
[docs] def crazy_stroke_image_binary(o, ar, avoidar): """Perform a milling operation using a binary image representation. This function implements a strategy for milling by navigating through a binary image. It starts from a defined point and attempts to move in various directions, evaluating the cutter load to determine the appropriate path. The algorithm continues until it either exhausts the available pixels to cut or reaches a predefined limit on the number of tests. The function modifies the input array to represent the areas that have been milled and returns the generated path as a list of chunks. Args: o (object): An object containing parameters for the milling operation, including cutter diameter, thresholds, and movement type. ar (np.ndarray): A 2D binary array representing the image to be milled. avoidar (np.ndarray): A 2D binary array indicating areas to avoid during milling. Returns: list: A list of chunks representing the path taken during the milling operation. """ # this surprisingly works, and can be used as a basis for something similar to adaptive milling strategy. # works like this: # start 'somewhere' # try to go in various directions. # if somewhere the cutter load is appropriate - it is correct magnitude and side, continue in that directon # try to continue straight or around that, looking minx, miny, minz, maxx, maxy, maxz = o.min.x, o.min.y, o.min.z, o.max.x, o.max.y, o.max.z # TODO this should be somewhere else, but here it is now to get at least some ambient for start of the operation. ar[: o.borderwidth, :] = 0 ar[-o.borderwidth :, :] = 0 ar[:, : o.borderwidth] = 0 ar[:, -o.borderwidth :] = 0 # ceil((o.cutter_diameter/12)/o.optimisation.pixsize) r = int((o.cutter_diameter / 2.0) / o.optimisation.pixsize) d = 2 * r coef = 0.75 maxarx = ar.shape[0] maxary = ar.shape[1] cutterArray = get_circle_binary(r) cutterArrayNegative = -cutterArray cutterimagepix = cutterArray.sum() anglelimit = o.crazy_threshold_3 # a threshold which says if it is valuable to cut in a direction satisfypix = cutterimagepix * o.crazy_threshold_1 toomuchpix = cutterimagepix * o.crazy_threshold_2 # same, but upper limit # (satisfypix+toomuchpix)/2.0# the ideal eating ratio optimalpix = cutterimagepix * o.crazy_threshold_5 indices = ar.nonzero() # first get white pixels startpix = ar.sum() # totpix = startpix chunk_builders = [] # try to find starting point here xs = indices[0][0] - r / 2 if xs < r: xs = r ys = indices[1][0] - r if ys < r: ys = r nchunk = CamPathChunkBuilder([(xs, ys)]) # startposition log.info(indices) log.info(f"{indices[0][0]}, {indices[1][0]}") # vector is 3d, blender somehow doesn't rotate 2d vectors with angles. lastvect = Vector((r, 0, 0)) # multiply *2 not to get values <1 pixel testvect = lastvect.normalized() * r / 4.0 rot = Euler((0, 0, 1)) i = 0 itests = 0 totaltests = 0 maxtests = 2000 maxtotaltests = 20000 # 1000000 margin = 0 # print(xs,ys,indices[0][0],indices[1][0],r) ar[xs - r : xs + r, ys - r : ys + r] = ( ar[xs - r : xs + r, ys - r : ys + r] * cutterArrayNegative ) anglerange = [-pi, pi] # range for angle of toolpath vector versus material vector - # probably direction negative to the force applied on cutter by material. testangleinit = 0 angleincrement = o.crazy_threshold_4 if (o.movement.type == "CLIMB" and o.movement.spindle_rotation == "CCW") or ( o.movement.type == "CONVENTIONAL" and o.movement.spindle_rotation == "CW" ): anglerange = [-pi, 0] testangleinit = anglelimit angleincrement = -angleincrement elif (o.movement.type == "CONVENTIONAL" and o.movement.spindle_rotation == "CCW") or ( o.movement.type == "CLIMB" and o.movement.spindle_rotation == "CW" ): anglerange = [0, pi] testangleinit = -anglelimit angleincrement = angleincrement while totpix > 0 and totaltests < maxtotaltests: # a ratio when the algorithm is allowed to end success = False # define a vector which gets varied throughout the testing, growing and growing angle to sides. testangle = testangleinit testleftright = False testlength = r foundsolutions = [] while not success: xs = int(nchunk.points[-1][0] + testvect.x) ys = int(nchunk.points[-1][1] + testvect.y) # print(xs,ys,ar.shape) # print(d) if ( xs > r + margin and xs < ar.shape[0] - r - margin and ys > r + margin and ys < ar.shape[1] - r - margin ): # avoidtest=avoidar[xs-r:xs+r,ys-r:ys+r]*cutterArray if not avoidar[xs, ys]: testar = ar[xs - r : xs + r, ys - r : ys + r] * cutterArray eatpix = testar.sum() cindices = testar.nonzero() cx = cindices[0].sum() / eatpix cy = cindices[1].sum() / eatpix v = Vector((cx - r, cy - r)) # print(testvect.length,testvect) if v.length != 0: angle = testvect.to_2d().angle_signed(v) if ( anglerange[0] < angle < anglerange[1] and toomuchpix > eatpix > satisfypix ) or (eatpix > 0 and totpix < startpix * 0.025): # this could be righthanded milling? # lets see :) # print(xs,ys,angle) foundsolutions.append([testvect.copy(), eatpix]) # or totpix < startpix*0.025: if len(foundsolutions) >= 10: success = True itests += 1 totaltests += 1 if success: # fist, try to inter/extrapolate the recieved results. closest = 100000000 for s in foundsolutions: if abs(s[1] - optimalpix) < closest: bestsolution = s closest = abs(s[1] - optimalpix) # v1#+(v2-v1)*ratio#rewriting with interpolated vect. testvect = bestsolution[0] xs = int(nchunk.points[-1][0] + testvect.x) ys = int(nchunk.points[-1][1] + testvect.y) nchunk.points.append([xs, ys]) lastvect = testvect ar[xs - r : xs + r, ys - r : ys + r] = ( ar[xs - r : xs + r, ys - r : ys + r] * cutterArrayNegative ) totpix -= bestsolution[1] itests = 0 totaltests = 0 else: # TODO: after all angles were tested into material higher than toomuchpix, # it should cancel, otherwise there is no problem with long travel in free space..... # TODO:the testing should start not from the same angle as lastvector, but more towards material. # So values closer to toomuchpix are obtained rather than satisfypix testvect = lastvect.normalized() * testlength if testangleinit == 0: # meander if testleftright: testangle = -testangle - angleincrement testleftright = False else: testangle = -testangle + angleincrement # increment angle testleftright = True else: # climb/conv. testangle += angleincrement if (abs(testangle) > o.crazy_threshold_3 and len(nchunk.points) > 1) or abs( testangle ) > 2 * pi: testangle = testangleinit testlength += r / 4.0 if nchunk.points[-1][0] + testvect.x < r: testvect.x = r if nchunk.points[-1][1] + testvect.y < r: testvect.y = r if nchunk.points[-1][0] + testvect.x > maxarx - r: testvect.x = maxarx - r if nchunk.points[-1][1] + testvect.y > maxary - r: testvect.y = maxary - r rot.z = testangle testvect.rotate(rot) if itests > maxtests or testlength > r * 1.5: andar = np.logical_and(ar, np.logical_not(avoidar)) indices = andar.nonzero() if len(nchunk.points) > 1: parent_child_distance([nchunk], chunks, o, distance=r) chunk_builders.append(nchunk) if totpix > startpix * 0.001: found = False ftests = 0 while not found: # look for next start point: index = random.randint(0, len(indices[0]) - 1) xs = indices[0][index] ys = indices[1][index] v = Vector((r - 1, 0, 0)) randomrot = random.random() * 2 * pi e = Euler((0, 0, randomrot)) v.rotate(e) xs += int(v.x) ys += int(v.y) if xs < r: xs = r if ys < r: ys = r if avoidar[xs, ys] == 0: testarsum = ( ar[xs - r : xs - r + d, ys - r : ys - r + d].sum() * pi / 4 ) if toomuchpix > testarsum > 0 or (totpix < startpix * 0.025): # 0 now instead of satisfypix found = True nchunk = CamPathChunk([(xs, ys)]) # startposition ar[xs - r : xs + r, ys - r : ys + r] = ( ar[xs - r : xs + r, ys - r : ys + r] * cutterArrayNegative ) # lastvect=Vector((r,0,0))#vector is 3d, # blender somehow doesn't rotate 2d vectors with angles. randomrot = random.random() * 2 * pi e = Euler((0, 0, randomrot)) testvect = lastvect.normalized() * 2 # multiply *2 not to get values <1 pixel testvect.rotate(e) lastvect = testvect.copy() if ftests > 2000: totpix = 0 # this quits the process now. ftests += 1 success = True itests = 0 i += 1 if i % 100 == 0: log.info("100 Succesfull Tests Done") totpix = ar.sum() log.info(totpix) log.info(totaltests) i = 0 if len(nchunk.points) > 1: parent_child_distance([nchunk], chunks, o, distance=r) chunk_builders.append(nchunk) for ch in chunk_builders: ch = ch.points for i in range(0, len(ch)): ch[i] = ( (ch[i][0] + coef - o.borderwidth) * o.optimisation.pixsize + minx, (ch[i][1] + coef - o.borderwidth) * o.optimisation.pixsize + miny, o.min_z, ) return [c.to_chunk for c in chunk_builders]
[docs] async def crazy_path(o): """Execute a greedy adaptive algorithm for path planning. This function prepares an area based on the provided object `o`, calculates the dimensions of the area, and initializes a mill image and cutter array. The dimensions are determined by the maximum and minimum coordinates of the object, adjusted by the simulation detail and border width. The function is currently a stub and requires further implementation. Args: o (object): An object containing properties such as max, min, optimisation, and borderwidth. Returns: None: This function does not return a value. """ # TODO: try to do something with this stuff, it's just a stub. It should be a greedy adaptive algorithm. # started another thing below. await prepare_area(o) sx = o.max.x - o.min.x sy = o.max.y - o.min.y resx = ceil(sx / o.optimisation.simulation_detail) + 2 * o.borderwidth resy = ceil(sy / o.optimisation.simulation_detail) + 2 * o.borderwidth o.millimage = numpy.full(shape=(resx, resy), fill_value=0.0, dtype=numpy.float) # getting inverted cutter o.cutterArray = -get_cutter_array(o, o.optimisation.simulation_detail)
[docs] def build_stroke(start, end, cutterArray): """Build a stroke array based on start and end points. This function generates a 2D stroke array that represents a stroke from a starting point to an ending point. It calculates the length of the stroke and creates a grid that is filled based on the positions defined by the start and end coordinates. The function uses a cutter array to determine how the stroke interacts with the grid. Args: start (tuple): A tuple representing the starting coordinates (x, y, z). end (tuple): A tuple representing the ending coordinates (x, y, z). cutterArray: An object that contains size information used to modify the stroke array. Returns: numpy.ndarray: A 2D array representing the stroke, filled with calculated values based on the input parameters. """ strokelength = max(abs(end[0] - start[0]), abs(end[1] - start[1])) size_x = abs(end[0] - start[0]) + cutterArray.size[0] size_y = abs(end[1] - start[1]) + cutterArray.size[0] r = cutterArray.size[0] / 2 strokeArray = numpy.full(shape=(size_x, size_y), fill_value=-10.0, dtype=numpy.float) samplesx = numpy.round(numpy.linspace(start[0], end[0], strokelength)) samplesy = numpy.round(numpy.linspace(start[1], end[1], strokelength)) samplesz = numpy.round(numpy.linspace(start[2], end[2], strokelength)) for i in range(0, len(strokelength)): strokeArray[samplesx[i] - r : samplesx[i] + r, samplesy[i] - r : samplesy[i] + r] = ( numpy.maximum( strokeArray[samplesx[i] - r : samplesx[i] + r, samplesy[i] - r : samplesy[i] + r], cutterArray + samplesz[i], ) ) return strokeArray
[docs] def test_stroke(): pass
[docs] def apply_stroke(): pass
[docs] def test_stroke_binary(img, stroke): pass # buildstroke()