# blender CAM strategy.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 strategy functionality of Blender CAM. The functions here are called with operators defined in ops.py.

import bpy
from bpy.props import *
import time
import math
from math import *
from bpy_extras import object_utils
from cam import chunk
from cam.chunk import *
from cam import collision
from cam.collision import *
from cam import simple
from cam.simple import *
from cam import pattern
from cam.pattern import *
from cam import utils, bridges, ops
from cam.utils import *
from cam import polygon_utils_cam
from cam.polygon_utils_cam import *
from cam import image_utils
from cam.image_utils import *

from shapely.geometry import polygon as spolygon
from shapely import geometry as sgeometry
from shapely import affinity

SHAPELY = True


# cutout strategy is completely here:
async def cutout(o):
    max_depth = checkminz(o)
    cutter_angle = math.radians(o.cutter_tip_angle / 2)
    c_offset = o.cutter_diameter / 2  # cutter ofset
    print("cuttertype:", o.cutter_type, "max_depth:", max_depth)
    if o.cutter_type == 'VCARVE':
        c_offset = -max_depth * math.tan(cutter_angle)
    elif o.cutter_type == 'CYLCONE':
        c_offset = -max_depth * math.tan(cutter_angle) + o.cylcone_diameter / 2
    elif o.cutter_type == 'BALLCONE':
        c_offset = -max_depth * math.tan(cutter_angle) + o.ball_radius
    elif o.cutter_type == 'BALLNOSE':
        r = o.cutter_diameter / 2
        print("cutter radius:", r," skin",o.skin)
        if -max_depth < r:
            c_offset = math.sqrt(r ** 2 - (r + max_depth) ** 2)
            print("offset:", c_offset)
    if c_offset > o.cutter_diameter / 2:
        c_offset = o.cutter_diameter / 2
    c_offset += o.skin  # add skin for profile
    if o.straight:
        join = 2
    else:
        join = 1
    print('operation: cutout')
    offset = True
    if o.cut_type == 'ONLINE' and o.onlycurves:  # is separate to allow open curves :)
        print('separate')
        chunksFromCurve = []
        for ob in o.objects:
            chunksFromCurve.extend(curveToChunks(ob, o.use_modifiers))
        # chunks always have polys now
        # for ch in chunksFromCurve:
        #     # print(ch.points)

        #     if len(ch.points) > 2:
        #         ch.poly = chunkToShapely(ch)

    # p.addContour(ch.poly)
    else:
        chunksFromCurve = []
        if o.cut_type == 'ONLINE':
            p = utils.getObjectOutline(0, o, True)

        else:
            offset = True
            if o.cut_type == 'INSIDE':
                offset = False

            p = utils.getObjectOutline(c_offset, o, offset)
            if o.outlines_count > 1:
                for i in range(1, o.outlines_count):
                    chunksFromCurve.extend(shapelyToChunks(p, -1))
                    path_distance = o.dist_between_paths
                    if o.cut_type == "INSIDE":
                        path_distance *= -1
                    p = p.buffer(distance = path_distance, resolution=o.optimisation.circle_detail, join_style=join,
                                 mitre_limit=2)

        chunksFromCurve.extend(shapelyToChunks(p, -1))
        if o.outlines_count > 1 and o.movement.insideout == 'OUTSIDEIN':
            chunksFromCurve.reverse()

    # parentChildPoly(chunksFromCurve,chunksFromCurve,o)
    chunksFromCurve = limitChunks(chunksFromCurve, o)
    if not o.dont_merge:
        parentChildPoly(chunksFromCurve, chunksFromCurve, o)
    if o.outlines_count == 1:
        chunksFromCurve = await utils.sortChunks(chunksFromCurve, o)

    if (o.movement.type == 'CLIMB' and o.movement.spindle_rotation == 'CCW') or (
            o.movement.type == 'CONVENTIONAL' and o.movement.spindle_rotation == 'CW'):
        for ch in chunksFromCurve:
            ch.reverse()

    if o.cut_type == 'INSIDE':  # there would bee too many conditions above,
        # so for now it gets reversed once again when inside cutting.
        for ch in chunksFromCurve:
            ch.reverse()

    layers = getLayers(o, o.maxz, checkminz(o))
    extendorder = []

    if o.first_down:  # each shape gets either cut all the way to bottom,
        # or every shape gets cut 1 layer, then all again. has to create copies,
        # because same chunks are worked with on more layers usually
        for chunk in chunksFromCurve:
            dir_switch = False  # needed to avoid unnecessary lifting of cutter with open chunks
            # and movement set to "MEANDER"
            for layer in layers:
                chunk_copy = chunk.copy()
                if dir_switch:
                    chunk_copy.reverse()
                extendorder.append([chunk_copy, layer])
                if (not chunk.closed) and o.movement.type == "MEANDER":
                    dir_switch = not dir_switch
    else:
        for layer in layers:
            for chunk in chunksFromCurve:
                extendorder.append([chunk.copy(), layer])

    for chl in extendorder:  # Set Z for all chunks
        chunk = chl[0]
        layer = chl[1]
        print(layer[1])
        chunk.setZ(layer[1])

    chunks = []

    if o.use_bridges:  # add bridges to chunks
        print('using bridges')
        simple.remove_multiple(o.name+'_cut_bridges')
        print("old briddge cut removed")

        bridgeheight = min(o.max.z, o.min.z + abs(o.bridges_height))

        for chl in extendorder:
            chunk = chl[0]
            layer = chl[1]
            if layer[1] < bridgeheight:
                bridges.useBridges(chunk, o)

    if o.profile_start > 0:
        print("cutout change profile start")
        for chl in extendorder:
            chunk = chl[0]
            if chunk.closed:
                chunk.changePathStart(o)

    # Lead in
    if o.lead_in > 0.0 or o.lead_out > 0:
        print("cutout leadin")
        for chl in extendorder:
            chunk = chl[0]
            if chunk.closed:
                chunk.breakPathForLeadinLeadout(o)
                chunk.leadContour(o)

    if o.movement.ramp:  # add ramps or simply add chunks
        for chl in extendorder:
            chunk = chl[0]
            layer = chl[1]
            if chunk.closed:
                chunk.rampContour(layer[0], layer[1], o)
                chunks.append(chunk)
            else:
                chunk.rampZigZag(layer[0], layer[1], o)
                chunks.append(chunk)
    else:
        for chl in extendorder:
            chunks.append(chl[0])

    chunksToMesh(chunks, o)


async def curve(o):
    print('operation: curve')
    pathSamples = []
    utils.getOperationSources(o)
    if not o.onlycurves:
        raise CamException("All objects must be curves for this operation.")

    for ob in o.objects:
        pathSamples.extend(curveToChunks(ob))  # make the chunks from curve here
    pathSamples = await utils.sortChunks(pathSamples, o)  # sort before sampling
    pathSamples = chunksRefine(pathSamples, o)  # simplify

    # layers here
    if o.use_layers:
        layers = getLayers(o, o.maxz, round(checkminz(o), 6))
        # layers is a list of lists [[0.00,l1],[l1,l2],[l2,l3]] containg the start and end of each layer
        extendorder = []
        chunks = []
        for layer in layers:
            for ch in pathSamples:
                extendorder.append([ch.copy(), layer])  # include layer information to chunk list

        for chl in extendorder:  # Set offset Z for all chunks according to the layer information,
            chunk = chl[0]
            layer = chl[1]
            print('layer: ' + str(layer[1]))
            chunk.offsetZ(o.maxz * 2 - o.minz + layer[1])
            chunk.clampZ(o.minz)  # safety to not cut lower than minz
            chunk.clampmaxZ(o.movement.free_height)  # safety, not higher than free movement height

        for chl in extendorder:  # strip layer information from extendorder and transfer them to chunks
            chunks.append(chl[0])

        chunksToMesh(chunks, o)  # finish by converting to mesh

    else:  # no layers, old curve
        for ch in pathSamples:
            ch.clampZ(o.minz)  # safety to not cut lower than minz
            ch.clampmaxZ(o.movement.free_height)  # safety, not higher than free movement height
        chunksToMesh(pathSamples, o)


async def proj_curve(s, o):
    print('operation: projected curve')
    pathSamples = []
    chunks = []
    ob = bpy.data.objects[o.curve_object]
    pathSamples.extend(curveToChunks(ob))

    targetCurve = s.objects[o.curve_object1]

    from cam import chunk
    if targetCurve.type != 'CURVE':
        raise CamException('Projection target and source have to be curve objects!')

    if 1:
        extend_up = 0.1
        extend_down = 0.04
        tsamples = curveToChunks(targetCurve)
        for chi, ch in enumerate(pathSamples):
            cht = tsamples[chi].get_points()
            ch.depth = 0
            ch_points=ch.get_points()
            for i, s in enumerate(ch_points):
                # move the points a bit
                ep = Vector(cht[i])
                sp = Vector(ch_points[i])
                # extend startpoint
                vecs = sp - ep
                vecs.normalize()
                vecs *= extend_up
                sp += vecs
                ch.startpoints.append(sp)

                # extend endpoint
                vece = sp - ep
                vece.normalize()
                vece *= extend_down
                ep -= vece
                ch.endpoints.append(ep)

                ch.rotations.append((0, 0, 0))

                vec = sp - ep
                ch.depth = min(ch.depth, -vec.length)
                ch_points[i] = sp.copy()
    ch.set_points(ch_points)
    layers = getLayers(o, 0, ch.depth)

    chunks.extend(utils.sampleChunksNAxis(o, pathSamples, layers))
    chunksToMesh(chunks, o)


async def pocket(o):
    print('operation: pocket')
    scene = bpy.context.scene

    simple.remove_multiple("3D_poc")

    max_depth = checkminz(o) + o.skin
    cutter_angle = math.radians(o.cutter_tip_angle / 2)
    c_offset = o.cutter_diameter / 2
    if o.cutter_type == 'VCARVE':
        c_offset = -max_depth * math.tan(cutter_angle)
    elif o.cutter_type == 'CYLCONE':
        c_offset = -max_depth * math.tan(cutter_angle) + o.cylcone_diameter / 2
    elif o.cutter_type == 'BALLCONE':
        c_offset = -max_depth * math.tan(cutter_angle) + o.ball_radius
    if c_offset > o.cutter_diameter / 2:
        c_offset = o.cutter_diameter / 2

    c_offset += o.skin  # add skin
    print("cutter offset", c_offset)

    p = utils.getObjectOutline(c_offset, o, False)
    approxn = (min(o.max.x - o.min.x, o.max.y - o.min.y) / o.dist_between_paths) / 2
    print("approximative:" + str(approxn))
    print(o)

    i = 0
    chunks = []
    chunksFromCurve = []
    lastchunks = []
    centers = None
    firstoutline = p  # for testing in the end.
    prest = p.buffer(-c_offset, o.optimisation.circle_detail)
    while not p.is_empty:
        if o.pocketToCurve:
            polygon_utils_cam.shapelyToCurve('3dpocket', p, 0.0)  # make a curve starting with _3dpocket

        nchunks = shapelyToChunks(p, o.min.z)
        # print("nchunks")
        pnew = p.buffer(-o.dist_between_paths, o.optimisation.circle_detail)
        if pnew.is_empty:

            pt = p.buffer(-c_offset, o.optimisation.circle_detail)     # test if the last curve will leave material
            if not pt.is_empty:
                pnew = pt
        # print("pnew")

        nchunks = limitChunks(nchunks, o)
        chunksFromCurve.extend(nchunks)
        parentChildDist(lastchunks, nchunks, o)
        lastchunks = nchunks

        percent = int(i / approxn * 100)
        progress('outlining polygons ', percent)
        p = pnew

        i += 1

    # if (o.poc)#TODO inside outside!
    if (o.movement.type == 'CLIMB' and o.movement.spindle_rotation == 'CW') or (
            o.movement.type == 'CONVENTIONAL' and o.movement.spindle_rotation == 'CCW'):
        for ch in chunksFromCurve:
            ch.reverse()

    chunksFromCurve = await utils.sortChunks(chunksFromCurve, o)

    chunks = []
    layers = getLayers(o, o.maxz, checkminz(o))

    for l in layers:
        lchunks = setChunksZ(chunksFromCurve, l[1])
        if o.movement.ramp:
            for ch in lchunks:
                ch.zstart = l[0]
                ch.zend = l[1]

        # helix_enter first try here TODO: check if helix radius is not out of operation area.
        if o.movement.helix_enter:
            helix_radius = c_offset * o.movement.helix_diameter * 0.01  # 90 percent of cutter radius
            helix_circumference = helix_radius * pi * 2

            revheight = helix_circumference * tan(o.movement.ramp_in_angle)
            for chi, ch in enumerate(lchunks):
                if not chunksFromCurve[chi].children:
                    p = ch.get_point(0)  # TODO:intercept closest next point when it should stay low
                    # first thing to do is to check if helix enter can really enter.
                    checkc = Circle(helix_radius + c_offset, o.optimisation.circle_detail)
                    checkc = affinity.translate(checkc, p[0], p[1])
                    covers = False
                    for poly in o.silhouete:
                        if poly.contains(checkc):
                            covers = True
                            break

                    if covers:
                        revolutions = (l[0] - p[2]) / revheight
                        # print(revolutions)
                        h = Helix(helix_radius, o.optimisation.circle_detail, l[0], p, revolutions)
                        # invert helix if not the typical direction
                        if (o.movement.type == 'CONVENTIONAL' and o.movement.spindle_rotation == 'CW') or (
                                o.movement.type == 'CLIMB' and o.movement.spindle_rotation == 'CCW'):
                            nhelix = []
                            for v in h:
                                nhelix.append((2 * p[0] - v[0], v[1], v[2]))
                            h = nhelix
                        ch.extend(h,at_index=0)
#                        ch.points = h + ch.points

                    else:
                        o.info.warnings += 'Helix entry did not fit! \n '
                        ch.closed = True
                        ch.rampZigZag(l[0], l[1], o)
        # Arc retract here first try:
        if o.movement.retract_tangential:  # TODO: check for entry and exit point before actual computing... will be much better.
            # TODO: fix this for CW and CCW!
            for chi, ch in enumerate(lchunks):
                # print(chunksFromCurve[chi])
                # print(chunksFromCurve[chi].parents)
                if chunksFromCurve[chi].parents == [] or len(chunksFromCurve[chi].parents) == 1:

                    revolutions = 0.25
                    v1 = Vector(ch.get_point(-1))
                    i = -2
                    v2 = Vector(ch.get_point(i))
                    v = v1 - v2
                    while v.length == 0:
                        i = i - 1
                        v2 = Vector(ch.get_point(i))
                        v = v1 - v2

                    v.normalize()
                    rotangle = Vector((v.x, v.y)).angle_signed(Vector((1, 0)))
                    e = Euler((0, 0, pi / 2.0))  # TODO:#CW CLIMB!
                    v.rotate(e)
                    p = v1 + v * o.movement.retract_radius
                    center = p
                    p = (p.x, p.y, p.z)

                    # progress(str((v1,v,p)))
                    h = Helix(o.movement.retract_radius, o.optimisation.circle_detail, p[2] + o.movement.retract_height, p, revolutions)

                    e = Euler((0, 0, rotangle + pi))  # angle to rotate whole retract move
                    rothelix = []
                    c = []  # polygon for outlining and checking collisions.
                    for p in h:  # rotate helix to go from tangent of vector
                        v1 = Vector(p)

                        v = v1 - center
                        v.x = -v.x  # flip it here first...
                        v.rotate(e)
                        p = center + v
                        rothelix.append(p)
                        c.append((p[0], p[1]))

                    c = sgeometry.Polygon(c)
                    # print('çoutline')
                    # print(c)
                    coutline = c.buffer(c_offset, o.optimisation.circle_detail)
                    # print(h)
                    # print('çoutline')
                    # print(coutline)
                    # polyToMesh(coutline,0)
                    rothelix.reverse()

                    covers = False
                    for poly in o.silhouete:
                        if poly.contains(coutline):
                            covers = True
                            break

                    if covers:
                        ch.extend(rothelix)

        chunks.extend(lchunks)

    if o.movement.ramp:
        for ch in chunks:
            ch.rampZigZag(ch.zstart, ch.get_point(0)[2], o)
    
    if o.first_down:
        if o.pocket_option == "OUTSIDE":
            chunks.reverse()   
        chunks = await utils.sortChunks(chunks, o)

    if o.pocketToCurve:  # make curve instead of a path
        simple.join_multiple("3dpocket")

    else:
        chunksToMesh(chunks, o)  # make normal pocket path


async def drill(o):
    print('operation: Drill')
    chunks = []
    for ob in o.objects:
        activate(ob)

        bpy.ops.object.duplicate_move(OBJECT_OT_duplicate={"linked": False, "mode": 'TRANSLATION'},
                                      TRANSFORM_OT_translate={"value": (0, 0, 0),
                                                              "constraint_axis": (False, False, False),
                                                              "orient_type": 'GLOBAL', "mirror": False,
                                                              "use_proportional_edit": False,
                                                              "proportional_edit_falloff": 'SMOOTH',
                                                              "proportional_size": 1, "snap": False,
                                                              "snap_target": 'CLOSEST', "snap_point": (0, 0, 0),
                                                              "snap_align": False, "snap_normal": (0, 0, 0),
                                                              "texture_space": False, "release_confirm": False})
        # bpy.ops.collection.objects_remove_all()
        bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM')

        ob = bpy.context.active_object
        if ob.type == 'CURVE':
            ob.data.dimensions = '3D'
        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
        l = ob.location

        if ob.type == 'CURVE':

            for c in ob.data.splines:
                maxx, minx, maxy, miny, maxz, minz = -10000, 10000, -10000, 10000, -10000, 10000
                for p in c.points:
                    if o.drill_type == 'ALL_POINTS':
                        chunks.append(camPathChunk([(p.co.x + l.x, p.co.y + l.y, p.co.z + l.z)]))
                    minx = min(p.co.x, minx)
                    maxx = max(p.co.x, maxx)
                    miny = min(p.co.y, miny)
                    maxy = max(p.co.y, maxy)
                    minz = min(p.co.z, minz)
                    maxz = max(p.co.z, maxz)
                for p in c.bezier_points:
                    if o.drill_type == 'ALL_POINTS':
                        chunks.append(camPathChunk([(p.co.x + l.x, p.co.y + l.y, p.co.z + l.z)]))
                    minx = min(p.co.x, minx)
                    maxx = max(p.co.x, maxx)
                    miny = min(p.co.y, miny)
                    maxy = max(p.co.y, maxy)
                    minz = min(p.co.z, minz)
                    maxz = max(p.co.z, maxz)
                cx = (maxx + minx) / 2
                cy = (maxy + miny) / 2
                cz = (maxz + minz) / 2

                center = (cx, cy)
                aspect = (maxx - minx) / (maxy - miny)
                if (1.3 > aspect > 0.7 and o.drill_type == 'MIDDLE_SYMETRIC') or o.drill_type == 'MIDDLE_ALL':
                    chunks.append(camPathChunk([(center[0] + l.x, center[1] + l.y, cz + l.z)]))

        elif ob.type == 'MESH':
            for v in ob.data.vertices:
                chunks.append(camPathChunk([(v.co.x + l.x, v.co.y + l.y, v.co.z + l.z)]))
        delob(ob)  # delete temporary object with applied transforms

    layers = getLayers(o, o.maxz, checkminz(o))

    chunklayers = []
    for layer in layers:
        for chunk in chunks:
            # If using object for minz then use z from points in object
            if o.minz_from == 'OBJECT':
                z = chunk.get_point(0)[2]
            else:  # using operation minz
                z = o.minz
            # only add a chunk layer if the chunk z point is in or lower than the layer
            if z <= layer[0]:
                if z <= layer[1]:
                    z = layer[1]
                # perform peck drill
                newchunk = chunk.copy()
                newchunk.setZ(z)
                chunklayers.append(newchunk)
                # retract tool to maxz (operation depth start in ui)
                newchunk = chunk.copy()
                newchunk.setZ(o.maxz)
                chunklayers.append(newchunk)

    chunklayers = await utils.sortChunks(chunklayers, o)
    chunksToMesh(chunklayers, o)


async def medial_axis(o):
    print('operation: Medial Axis')

    simple.remove_multiple("medialMesh")

    from cam.voronoi import Site, computeVoronoiDiagram

    chunks = []

    gpoly = spolygon.Polygon()
    angle = o.cutter_tip_angle
    slope = math.tan(math.pi * (90 - angle / 2) / 180)  # angle in degrees
    # slope = math.tan((math.pi-angle)/2) #angle in radian
    new_cutter_diameter = o.cutter_diameter
    m_o_ob = o.object_name
    if o.cutter_type == 'VCARVE':
        angle = o.cutter_tip_angle
        # start the max depth calc from the "start depth" of the operation.
        maxdepth = o.maxz - slope * o.cutter_diameter / 2 - o.skin
        # don't cut any deeper than the "end depth" of the operation.
        if maxdepth < o.minz:
            maxdepth = o.minz
            # the effective cutter diameter can be reduced from it's max
            # since we will be cutting shallower than the original maxdepth
            # without this, the curve is calculated as if the diameter was at the original maxdepth and we get the bit
            # pulling away from the desired cut surface
            new_cutter_diameter = (maxdepth - o.maxz) / (- slope) * 2
    elif o.cutter_type == 'BALLNOSE':
        maxdepth = - new_cutter_diameter / 2 - o.skin
    else:
        raise CamException("Only Ballnose and V-carve cutters are supported for meial axis.")
    # remember resolutions of curves, to refine them,
    # otherwise medial axis computation yields too many branches in curved parts
    resolutions_before = []

    for ob in o.objects:
        if ob.type == 'CURVE' or ob.type == 'FONT':
            resolutions_before.append(ob.data.resolution_u)
            if ob.data.resolution_u < 64:
                ob.data.resolution_u = 64

    polys = utils.getOperationSilhouete(o)
    if isinstance(polys, list):
        if len(polys)==1 and isinstance(polys[0],shapely.MultiPolygon):
            mpoly=polys[0]
        else:
            mpoly=sgeometry.MultiPolygon(polys)
    elif isinstance(polys,shapely.MultiPolygon):
        # just a multipolygon
        mpoly=polys
    else:
        raise CamException("Failed getting object silhouette. Is input curve closed?")
    
    mpoly_boundary = mpoly.boundary
    ipol = 0
    for poly in mpoly.geoms:
        ipol = ipol + 1
        schunks = shapelyToChunks(poly, -1)
        schunks = chunksRefineThreshold(schunks, o.medial_axis_subdivision,
                                        o.medial_axis_threshold)  # chunksRefine(schunks,o)

        verts = []
        for ch in schunks:
            verts.extend(ch.get_points())
            # for pt in ch.get_points():
            #     # pvoro = Site(pt[0], pt[1])
            #     verts.append(pt)  # (pt[0], pt[1]), pt[2])
        # verts= points#[[vert.x, vert.y, vert.z] for vert in vertsPts]
        nDupli, nZcolinear = unique(verts)
        nVerts = len(verts)
        print(str(nDupli) + " duplicates points ignored")
        print(str(nZcolinear) + " z colinear points excluded")
        if nVerts < 3:
            print("Not enough points")
            return {'FINISHED'}
        # Check colinear
        xValues = [pt[0] for pt in verts]
        yValues = [pt[1] for pt in verts]
        if checkEqual(xValues) or checkEqual(yValues):
            print("Points are colinear")
            return {'FINISHED'}
        # Create diagram
        print("Tesselation... (" + str(nVerts) + " points)")
        xbuff, ybuff = 5, 5  # %
        zPosition = 0
        vertsPts = [Point(vert[0], vert[1], vert[2]) for vert in verts]
        # vertsPts= [Point(vert[0], vert[1]) for vert in verts]

        pts, edgesIdx = computeVoronoiDiagram(vertsPts, xbuff, ybuff, polygonsOutput=False, formatOutput=True)

        # pts=[[pt[0], pt[1], zPosition] for pt in pts]
        newIdx = 0
        vertr = []
        filteredPts = []
        print('filter points')
        ipts = 0
        for p in pts:
            ipts = ipts + 1
            if ipts % 500 == 0:
                sys.stdout.write('\r')
                # the exact output you're looking for:
                prog_message = "points: " + str(ipts) + " / " + str(len(pts)) + " " + str(
                    round(100 * ipts / len(pts))) + "%"
                sys.stdout.write(prog_message)
                sys.stdout.flush()

            if not poly.contains(sgeometry.Point(p)):
                vertr.append((True, -1))
            else:
                vertr.append((False, newIdx))
                if o.cutter_type == 'VCARVE':
                    # start the z depth calc from the "start depth" of the operation.
                    z = o.maxz - mpoly.boundary.distance(sgeometry.Point(p)) * slope
                    if z < maxdepth:
                        z = maxdepth
                elif o.cutter_type == 'BALL' or o.cutter_type == 'BALLNOSE':
                    d = mpoly_boundary.distance(sgeometry.Point(p))
                    r = new_cutter_diameter / 2.0
                    if d >= r:
                        z = -r
                    else:
                        # print(r, d)
                        z = -r + sqrt(r * r - d * d)
                else:
                    z = 0  #
                # print(mpoly.distance(sgeometry.Point(0,0)))
                # if(z!=0):print(z)
                filteredPts.append((p[0], p[1], z))
                newIdx += 1

        print('filter edges')
        filteredEdgs = []
        ledges = []
        for e in edgesIdx:
            do = True
            # p1 = pts[e[0]]
            # p2 = pts[e[1]]
            # print(p1,p2,len(vertr))
            if vertr[e[0]][0]:  # exclude edges with allready excluded points
                do = False
            elif vertr[e[1]][0]:
                do = False
            if do:
                filteredEdgs.append((vertr[e[0]][1], vertr[e[1]][1]))
                ledges.append(sgeometry.LineString((filteredPts[vertr[e[0]][1]], filteredPts[vertr[e[1]][1]])))
        # print(ledges[-1].has_z)

        bufpoly = poly.buffer(-new_cutter_diameter / 2, resolution=64)

        lines = shapely.ops.linemerge(ledges)
        # print(lines.type)

        if bufpoly.type == 'Polygon' or bufpoly.type == 'MultiPolygon':
            lines = lines.difference(bufpoly)
            chunks.extend(shapelyToChunks(bufpoly, maxdepth))
        chunks.extend(shapelyToChunks(lines, 0))

        # generate a mesh from the medial calculations
        if o.add_mesh_for_medial:
            polygon_utils_cam.shapelyToCurve('medialMesh', lines, 0.0)
            bpy.ops.object.convert(target='MESH')

    oi = 0
    for ob in o.objects:
        if ob.type == 'CURVE' or ob.type == 'FONT':
            ob.data.resolution_u = resolutions_before[oi]
            oi += 1

    # bpy.ops.object.join()
    chunks = await utils.sortChunks(chunks, o)

    layers = getLayers(o, o.maxz, o.min.z)

    chunklayers = []

    for layer in layers:
        for chunk in chunks:
            if chunk.isbelowZ(layer[0]):
                newchunk = chunk.copy()
                newchunk.clampZ(layer[1])
                chunklayers.append(newchunk)

    if o.first_down:
        chunklayers = await utils.sortChunks(chunklayers, o)

    if o.add_mesh_for_medial:  # make curve instead of a path
        simple.join_multiple("medialMesh")

    chunksToMesh(chunklayers, o)
    # add pocket operation for medial if add pocket checked
    if o.add_pocket_for_medial:
        #        o.add_pocket_for_medial = False
        # export medial axis parameter to pocket op
        ops.Add_Pocket(None, maxdepth, m_o_ob, new_cutter_diameter)


def getLayers(operation, startdepth, enddepth):
    """returns a list of layers bounded by startdepth and enddepth
       uses operation.stepdown to determine number of layers.
    """
    if startdepth < enddepth:
        raise CamException("Start depth is lower than end depth. "
        "If you have set a custom depth end, it must be lower than depth start, "
        "and should usually be negative. Set this in the CAM Operation Area panel.")
    if operation.use_layers:
        layers = []
        n = math.ceil((startdepth - enddepth) / operation.stepdown)
        print("start " + str(startdepth) + " end " + str(enddepth) + " n " + str(n))

        layerstart = operation.maxz
        for x in range(0, n):
            layerend = round(max(startdepth - ((x + 1) * operation.stepdown), enddepth), 6)
            if int(layerstart * 10 ** 8) != int(layerend * 10 ** 8):
                # it was possible that with precise same end of operation,
                # last layer was done 2x on exactly same level...
                layers.append([layerstart, layerend])
            layerstart = layerend
    else:
        layers = [[round(startdepth, 6), round(enddepth, 6)]]

    return layers


def chunksToMesh(chunks, o):
    """convert sampled chunks to path, optimization of paths"""
    t = time.time()
    s = bpy.context.scene
    m = s.cam_machine
    verts = []

    free_height = o.movement.free_height  # o.max.z +

    if o.machine_axes == '3':
        if m.use_position_definitions:
            origin = (m.starting_position.x, m.starting_position.y, m.starting_position.z)  # dhull
        else:
            origin = (0, 0, free_height)

        verts = [origin]
    if o.machine_axes != '3':
        verts_rotations = []  # (0,0,0)
    if (o.machine_axes == '5' and o.strategy5axis == 'INDEXED') or (
            o.machine_axes == '4' and o.strategy4axis == 'INDEXED'):
        extendChunks5axis(chunks, o)

    if o.array:
        nchunks = []
        for x in range(0, o.array_x_count):
            for y in range(0, o.array_y_count):
                print(x, y)
                for ch in chunks:
                    ch = ch.copy()
                    ch.shift(x * o.array_x_distance, y * o.array_y_distance, 0)
                    nchunks.append(ch)
        chunks = nchunks

    progress('building paths from chunks')
    e = 0.0001
    lifted = True

    for chi in range(0, len(chunks)):

        ch = chunks[chi]
        # print(chunks)
        # print (ch)
        if ch.count() > 0:  # TODO: there is a case where parallel+layers+zigzag ramps send empty chunks here...
            # print(len(ch.points))
            nverts = []
            if o.optimisation.optimize:
                ch = optimizeChunk(ch, o)

            # lift and drop

            if lifted:  # did the cutter lift before? if yes, put a new position above of the first point of next chunk.
                if o.machine_axes == '3' or (o.machine_axes == '5' and o.strategy5axis == 'INDEXED') or (
                        o.machine_axes == '4' and o.strategy4axis == 'INDEXED'):
                    v = (ch.get_point(0)[0], ch.get_point(0)[1], free_height)
                else:  # otherwise, continue with the next chunk without lifting/dropping
                    v = ch.startpoints[0]  # startpoints=retract points
                    verts_rotations.append(ch.rotations[0])
                verts.append(v)

            # add whole chunk
            verts.extend(ch.get_points())

            # add rotations for n-axis
            if o.machine_axes != '3':
                verts_rotations.extend(ch.rotations)

            lift = True
            # check if lifting should happen
            if chi < len(chunks) - 1 and chunks[chi + 1].count() > 0:
                # TODO: remake this for n axis, and this check should be somewhere else...
                last = Vector(ch.get_point(-1))
                first = Vector(chunks[chi + 1].get_point(0))
                vect = first - last
                if (o.machine_axes == '3' and (o.strategy == 'PARALLEL' or o.strategy == 'CROSS')
                    and vect.z == 0 and vect.length < o.dist_between_paths * 2.5) \
                        or (o.machine_axes == '4' and vect.length < o.dist_between_paths * 2.5):
                    # case of neighbouring paths
                    lift = False
                if abs(vect.x) < e and abs(vect.y) < e:  # case of stepdown by cutting.
                    lift = False

            if lift:
                if o.machine_axes == '3' or (o.machine_axes == '5' and o.strategy5axis == 'INDEXED') or (
                        o.machine_axes == '4' and o.strategy4axis == 'INDEXED'):
                    v = (ch.get_point(-1)[0], ch.get_point(-1)[1], free_height)
                else:
                    v = ch.startpoints[-1]
                    verts_rotations.append(ch.rotations[-1])
                verts.append(v)
            lifted = lift
    # print(verts_rotations)
    if o.optimisation.use_exact and not o.optimisation.use_opencamlib:
        cleanupBulletCollision(o)
    print(time.time() - t)
    t = time.time()

    # actual blender object generation starts here:
    edges = []
    for a in range(0, len(verts) - 1):
        edges.append((a, a + 1))

    oname = "cam_path_{}".format(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
        ob = s.objects[oname]
    else:
        ob = object_utils.object_data_add(bpy.context, mesh, operator=None)

    if o.machine_axes != '3':
        # store rotations into shape keys, only way to store large arrays with correct floating point precision
        # - object/mesh attributes can only store array up to 32000 intems.

        ob.shape_key_add()
        ob.shape_key_add()
        shapek = mesh.shape_keys.key_blocks[1]
        shapek.name = 'rotations'
        print(len(shapek.data))
        print(len(verts_rotations))

        for i, co in enumerate(verts_rotations):  # TODO: optimize this. this is just rewritten too many times...
            shapek.data[i].co = co

    print(time.time() - t)

    ob.location = (0, 0, 0)
    o.path_object_name = oname

    # parent the path object to source object if object mode
    if (o.geometry_source == 'OBJECT') and o.parent_path_to_object:
        activate(o.objects[0])
        ob.select_set(state=True, view_layer=None)
        bpy.ops.object.parent_set(type='OBJECT', keep_transform=True)
    else:
        ob.select_set(state=True, view_layer=None)


def checkminz(o):
    if o.minz_from == 'MATERIAL':
        return o.min.z
    else:
        return o.minz
