Source code for fabex.toolpath

"""Fabex 'toolpath.py' © 2012 Vilem Novak

Generate toolpaths from path chunks.
"""

# G-code Generaton
from math import (
    ceil,
    floor,
)
import time

from shapely.geometry import Polygon

import bpy

from .bridges import use_bridges
from .exception import CamException
from .constants import (
    USE_PROFILER,
)
from .exception import CamException
from .gcode.gcode_export import export_gcode_path
from .pattern import get_path_pattern, get_path_pattern_4_axis

from .strategies.cutout import cutout
from .strategies.curve_to_path import curve
from .strategies.drill import drill
from .strategies.medial_axis import medial_axis
from .strategies.project_curve import project_curve
from .strategies.pocket import pocket

from .utilities.async_utils import progress_async
from .utilities.bounds_utils import get_bounds
from .utilities.chunk_utils import (
    chunks_to_mesh,
    chunks_refine,
    limit_chunks,
    chunks_coherency,
    sample_chunks,
    sample_chunks_n_axis,
    connect_chunks_low,
    sort_chunks,
)
from .utilities.curve_utils import curve_to_chunks
from .utilities.image_utils import (
    prepare_area,
    get_offset_image_cavities,
)
from .utilities.image_shapely_utils import image_to_shapely
from .utilities.index_utils import (
    cleanup_indexed,
    prepare_indexed,
)
from .utilities.logging_utils import log
from .utilities.operation_utils import (
    get_ambient,
    get_operation_sources,
    get_change_data,
    check_memory_limit,
    get_layers,
)
from .utilities.parent_utils import parent_child_distance
from .utilities.shapely_utils import (
    shapely_to_chunks,
)
from .utilities.silhouette_utils import get_operation_silhouette
from .utilities.simple_utils import progress
from .utilities.stroke_utils import crazy_stroke_image_binary
from .utilities.waterline_utils import oclGetWaterline


[docs] async def get_path(context, operation): """Calculate the path for a given operation in a specified context. This function performs various calculations to determine the path based on the operation's parameters and context. It checks for changes in the operation's data and updates relevant tags accordingly. Depending on the number of machine axes specified in the operation, it calls different functions to handle 3-axis, 4-axis, or 5-axis operations. Additionally, if automatic export is enabled, it exports the generated G-code path. Args: context: The context in which the operation is being performed. operation: An object representing the operation with various attributes such as machine_axes, strategy, and auto_export. """ # should do all path calculations. t = time.process_time() if operation.feedrate > context.scene.cam_machine.feedrate_max: raise CamException("Operation Feedrate is greater than Machine Maximum!") # these tags are for caching of some of the results. Not working well still # - although it can save a lot of time during calculation... chd = get_change_data(operation) if operation.change_data != chd: # or 1: operation.update_offset_image_tag = True operation.update_z_buffer_image_tag = True operation.change_data = chd operation.update_silhouette_tag = True operation.update_ambient_tag = True operation.update_bullet_collision_tag = True indexed_five_axis = operation.machine_axes == "5" and operation.strategy_5_axis == "INDEXED" indexed_four_axis = operation.machine_axes == "4" and operation.strategy_4_axis == "INDEXED" get_operation_sources(operation) operation.info.warnings = "" check_memory_limit(operation) log.info(f"Operation Axes: {operation.machine_axes}") if operation.machine_axes == "3": if USE_PROFILER == True: # profiler import cProfile pr = cProfile.Profile() pr.enable() await get_path_3_axis(context, operation) pr.disable() pr.dump_stats(time.strftime("Fabex_%Y%m%d_%H%M.prof")) else: await get_path_3_axis(context, operation) elif indexed_five_axis or indexed_four_axis: # 5 axis operations are now only 3 axis operations that get rotated... operation.orientation = prepare_indexed(operation) # TODO RENAME THIS await get_path_3_axis(context, operation) # TODO RENAME THIS cleanup_indexed(operation) # TODO RENAME THIS # transform5axisIndexed elif operation.machine_axes == "4": await get_path_4_axis(context, operation) # export gcode if automatic. if operation.auto_export: path_name = context.scene.cam_names.path_name_full if bpy.data.objects.get(path_name) is None: return p = bpy.data.objects[path_name] name = operation.name if operation.link_operation_file_names else operation.filename export_gcode_path(name, [p.data], [operation]) operation.changed = False t1 = time.process_time() - t progress("Total Time: ", t1)
# this is the main function. # FIXME: split strategies into separate file!
[docs] async def get_path_3_axis(context, operation): """Generate a machining path based on the specified operation strategy. This function evaluates the provided operation's strategy and generates the corresponding machining path. It supports various strategies such as 'CUTOUT', 'CURVE', 'PROJECTED_CURVE', 'POCKET', and others. Depending on the strategy, it performs specific calculations and manipulations on the input data to create a path that can be used for machining operations. The function handles different strategies by calling appropriate methods from the `strategy` module and processes the path samples accordingly. It also manages the generation of chunks, which represent segments of the machining path, and applies any necessary transformations based on the operation's parameters. Args: context (bpy.context): The Blender context containing scene information. operation (Operation): An object representing the machining operation, which includes strategy and other relevant parameters. Returns: None: This function does not return a value but modifies the state of the operation and context directly. """ s = bpy.context.scene o = operation get_bounds(o) tw = time.time() # strategy_from_operation = { # "CUTOUT": cutout(o), # "CURVE": curve(o), # "DRILL": strategy.drill(o), # "MEDIAL_AXIS": strategy.medial_axis(o), # "PROJECTED_CURVE": strategy.project_curve(s, o), # "POCKET": strategy.pocket(o), # } # await strategy_from_operation[o.strategy] if o.strategy == "CUTOUT": await cutout(o) elif o.strategy == "CURVE": await curve(o) elif o.strategy == "DRILL": await drill(o) elif o.strategy == "MEDIAL_AXIS": await medial_axis(o) elif o.strategy == "PROJECTED_CURVE": await project_curve(s, o) elif o.strategy == "POCKET": await pocket(o) elif o.strategy in [ "PARALLEL", "CROSS", "BLOCK", "SPIRAL", "CIRCLES", "OUTLINEFILL", "CARVE", "PENCIL", "CRAZY", ]: if o.strategy == "CARVE": pathSamples = [] ob = bpy.data.objects[o.curve_source] pathSamples.extend(curve_to_chunks(ob)) # sort before sampling pathSamples = await sort_chunks(pathSamples, o) pathSamples = chunks_refine(pathSamples, o) elif o.strategy == "PENCIL": await prepare_area(o) get_ambient(o) pathSamples = get_offset_image_cavities(o, o.offset_image) pathSamples = limit_chunks(pathSamples, o) # sort before sampling pathSamples = await sort_chunks(pathSamples, o) elif o.strategy == "CRAZY": await prepare_area(o) # pathSamples = crazyStrokeImage(o) # this kind of worked and should work: millarea = o.zbuffer_image < o.min_z + 0.000001 avoidarea = o.offset_image > o.min_z + 0.000001 pathSamples = crazy_stroke_image_binary(o, millarea, avoidarea) ##### pathSamples = await sort_chunks(pathSamples, o) pathSamples = chunks_refine(pathSamples, o) else: if o.strategy == "OUTLINEFILL": get_operation_silhouette(o) pathSamples = get_path_pattern(o) if o.strategy == "OUTLINEFILL": pathSamples = await sort_chunks(pathSamples, o) # have to be sorted once before, because of the parenting inside of samplechunks if o.strategy in ["BLOCK", "SPIRAL", "CIRCLES"]: pathSamples = await connect_chunks_low(pathSamples, o) chunks = [] layers = get_layers(o, o.max_z, o.min.z) log.info(f"Sampling Object: {o.name}") chunks.extend(await sample_chunks(o, pathSamples, layers)) log.info("Sampling Finished Successfully") if o.strategy == "PENCIL": chunks = chunks_coherency(chunks) log.info("Coherency Check") if o.strategy in ["PARALLEL", "CROSS", "PENCIL", "OUTLINEFILL"]: log.info("Sorting") chunks = await sort_chunks(chunks, o) if o.strategy == "OUTLINEFILL": chunks = await connect_chunks_low(chunks, o) if o.movement.ramp: for ch in chunks: ch.ramp_zig_zag(ch.zstart, None, o) if o.strategy == "CARVE": for ch in chunks: ch.offset_z(-o.carve_depth) if o.use_bridges: log.info(chunks) for bridge_chunk in chunks: use_bridges(bridge_chunk, o) chunks_to_mesh(chunks, o) elif o.strategy == "WATERLINE" and o.optimisation.use_opencamlib: get_ambient(o) chunks = [] await oclGetWaterline(o, chunks) chunks = limit_chunks(chunks, o) 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 chunks: ch.reverse() chunks_to_mesh(chunks, o) elif o.strategy == "WATERLINE" and not o.optimisation.use_opencamlib: topdown = True chunks = [] await progress_async("Retrieving Object Slices") await prepare_area(o) layerstep = 1000000000 if o.use_layers: layerstep = floor(o.stepdown / o.slice_detail) if layerstep == 0: layerstep = 1 # for projection of filled areas layerstart = o.max.z # layerend = o.min.z # layers = [[layerstart, layerend]] ####################### nslices = ceil(abs((o.min_z - o.max_z) / o.slice_detail)) lastslice = Polygon() # polyversion layerstepinc = 0 slicesfilled = 0 get_ambient(o) for h in range(0, nslices): layerstepinc += 1 slicechunks = [] # lower the layer by the skin value so the slice gets done at the tip of the tool z = o.min_z + h * o.slice_detail - o.skin if h == 0: z += 0.0000001 # if people do mill flat areas, this helps to reach those... # otherwise first layer would actually be one slicelevel above min z. islice = o.offset_image > z slicepolys = image_to_shapely(o, islice, with_border=True) poly = Polygon() # polygversion lastchunks = [] for p in slicepolys.geoms: poly = poly.union(p) # polygversion TODO: why is this added? nchunks = shapely_to_chunks(p, z + o.skin) nchunks = limit_chunks(nchunks, o, force=True) lastchunks.extend(nchunks) slicechunks.extend(nchunks) if len(slicepolys.geoms) > 0: slicesfilled += 1 # if o.waterline_fill: layerstart = min(o.max_z, z + o.slice_detail) # layerend = max(o.min.z, z - o.slice_detail) # layers = [[layerstart, layerend]] ##################################### # fill top slice for normal and first for inverse, fill between polys if not lastslice.is_empty or ( o.inverse and not poly.is_empty and slicesfilled == 1 ): restpoly = None if not lastslice.is_empty: # between polys if o.inverse: restpoly = poly.difference(lastslice) else: restpoly = lastslice.difference(poly) if (not o.inverse and poly.is_empty and slicesfilled > 0) or ( o.inverse and not poly.is_empty and slicesfilled == 1 ): # first slice fill restpoly = lastslice restpoly = restpoly.buffer( -o.distance_between_paths, resolution=o.optimisation.circle_detail ) fillz = z i = 0 while not restpoly.is_empty: nchunks = shapely_to_chunks(restpoly, fillz + o.skin) # project paths TODO: path projection during waterline is not working if o.waterline_project: nchunks = chunks_refine(nchunks, o) nchunks = await sample_chunks(o, nchunks, layers) nchunks = limit_chunks(nchunks, o, force=True) ######################### slicechunks.extend(nchunks) parent_child_distance(lastchunks, nchunks, o) lastchunks = nchunks restpoly = restpoly.buffer( -o.distance_between_paths, resolution=o.optimisation.circle_detail ) i += 1 i = 0 # fill layers and last slice, last slice with inverse is not working yet # - inverse millings end now always on 0 so filling ambient does have no sense. if ( (slicesfilled > 0 and layerstepinc == layerstep) or (not o.inverse and not poly.is_empty and slicesfilled == 1) or (o.inverse and poly.is_empty and slicesfilled > 0) ): fillz = z layerstepinc = 0 bound_rectangle = o.ambient restpoly = bound_rectangle.difference(poly) if o.inverse and poly.is_empty and slicesfilled > 0: restpoly = bound_rectangle.difference(lastslice) restpoly = restpoly.buffer( -o.distance_between_paths, resolution=o.optimisation.circle_detail ) i = 0 while not restpoly.is_empty: nchunks = shapely_to_chunks(restpoly, fillz + o.skin) ######################### nchunks = limit_chunks(nchunks, o, force=True) slicechunks.extend(nchunks) parent_child_distance(lastchunks, nchunks, o) lastchunks = nchunks restpoly = restpoly.buffer( -o.distance_between_paths, resolution=o.optimisation.circle_detail ) i += 1 percent = int(h / nslices * 100) await progress_async("Waterline Layers", percent) lastslice = poly if (o.movement.type == "CONVENTIONAL" and o.movement.spindle_rotation == "CCW") or ( o.movement.type == "CLIMB" and o.movement.spindle_rotation == "CW" ): for chunk in slicechunks: chunk.reverse() slicechunks = await sort_chunks(slicechunks, o) if topdown: slicechunks.reverse() # project chunks in between chunks.extend(slicechunks) if topdown: chunks.reverse() chunks_to_mesh(chunks, o) await progress_async(f"Done", time.time() - tw, "s")
[docs] async def get_path_4_axis(context, operation): """Generate a path for a specified axis based on the given operation. This function retrieves the bounds of the operation and checks the strategy associated with the axis. If the strategy is one of the specified types ('PARALLELR', 'PARALLEL', 'HELIX', 'CROSS'), it generates path samples and processes them into chunks for meshing. The function utilizes various helper functions to achieve this, including obtaining layers and sampling chunks. Args: context: The context in which the operation is executed. operation: An object that contains the strategy and other necessary parameters for generating the path. Returns: None: This function does not return a value but modifies the state of the operation by processing chunks for meshing. """ o = operation get_bounds(o) if o.strategy_4_axis in ["PARALLELR", "PARALLEL", "HELIX", "CROSS"]: path_samples = get_path_pattern_4_axis(o) depth = path_samples[0].depth chunks = [] layers = get_layers(o, 0, depth) chunks.extend(await sample_chunks_n_axis(o, path_samples, layers)) chunks_to_mesh(chunks, o)