Source code for cam.utilities.ocl_utils

"""Fabex 'ocl_utils.py'

Functions used by OpenCAMLib sampling.
"""

from math import radians, tan
import os
from subprocess import call
import tempfile

import numpy as np

try:
    import ocl
except ImportError:
    try:
        import opencamlib as ocl
    except ImportError:
        pass

import bpy

try:
    from bl_ext.blender_org.stl_format_legacy import blender_utils
except ImportError:
    pass
import mathutils

from ..constants import (
    BULLET_SCALE,
    OCL_SCALE,
    PYTHON_BIN,
    _PREVIOUS_OCL_MESH,
)
from ..exception import CamException
from .async_utils import progress_async
from .simple_utils import activate


[docs] def pointSamplesFromOCL(points, samples): """Update the z-coordinate of points based on corresponding sample values. This function iterates over a list of points and updates the z-coordinate of each point using the z value from the corresponding sample. The z value is scaled by a predefined constant, OCL_SCALE. It is assumed that the length of the points list matches the length of the samples list. Args: points (list): A list of points, where each point is expected to be a list or array with at least three elements. samples (list): A list of sample objects, where each sample is expected to have a z attribute. """ for index, point in enumerate(points): point[2] = samples[index].z / OCL_SCALE
[docs] def chunkPointSamplesFromOCL(chunks, samples): """Chunk point samples from OCL. This function processes a list of chunks and corresponding samples, extracting the z-values from the samples and scaling them according to a predefined constant (OCL_SCALE). It sets the scaled z-values for each chunk based on the number of points in that chunk. Args: chunks (list): A list of chunk objects that have a method `count()` and a method `set_z()`. samples (list): A list of sample objects from which z-values are extracted. """ s_index = 0 for ch in chunks: ch_points = ch.count() z_vals = np.array([p.z for p in samples[s_index : s_index + ch_points]]) z_vals /= OCL_SCALE ch.set_z(z_vals) s_index += ch_points
[docs] def chunkPointsResampleFromOCL(chunks, samples): """Resample the Z values of points in chunks based on provided samples. This function iterates through a list of chunks and resamples the Z values of the points in each chunk using the corresponding samples. It first counts the number of points in each chunk, then extracts the Z values from the samples, scales them by a predefined constant (OCL_SCALE), and sets the resampled Z values back to the chunk. Args: chunks (list): A list of chunk objects, each containing points that need to be resampled. samples (list): A list of sample objects from which Z values are extracted. """ s_index = 0 for ch in chunks: ch_points = ch.count() z_vals = np.array([p.z for p in samples[s_index : s_index + ch_points]]) z_vals /= OCL_SCALE ch.set_z(z_vals) s_index += ch_points
[docs] def exportModelsToSTL(operation): """Export models to STL format. This function takes an operation containing a collection of collision objects and exports each object as an STL file. It duplicates each object, applies transformations, and resizes them according to a predefined scale before exporting them to the temporary directory. The exported files are named sequentially as "model0.stl", "model1.stl", etc. After exporting, the function deletes the duplicated objects to clean up the scene. Args: operation: An object containing a collection of collision objects to be exported. """ file_number = 0 for collision_object in operation.objects: activate(collision_object) bpy.ops.object.duplicate(linked=False) # collision_object = bpy.context.scene.objects.active # bpy.context.scene.objects.selected = collision_object file_name = os.path.join(tempfile.gettempdir(), "model{0}.stl".format(str(file_number))) bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) bpy.ops.transform.resize( value=(OCL_SCALE, OCL_SCALE, OCL_SCALE), 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.object.transform_apply(location=True, rotation=True, scale=True) bpy.ops.export_mesh.stl( check_existing=True, filepath=file_name, filter_glob="*.stl", use_selection=True, ascii=False, use_mesh_modifiers=True, axis_forward="Y", axis_up="Z", global_scale=1.0, ) bpy.ops.object.delete() file_number += 1
[docs] async def oclSamplePoints(operation, points): """Sample points using an operation and process the results. This asynchronous function takes an operation and a set of points, samples the points using the specified operation, and then processes the sampled points. The function relies on an external sampling function and a processing function to handle the sampling and post-processing of the data. Args: operation (str): The operation to be performed on the points. points (list): A list of points to be sampled. """ samples = await ocl_sample(operation, points) pointSamplesFromOCL(points, samples)
[docs] async def oclSample(operation, chunks): """Perform an operation on a set of chunks and process the resulting samples. This asynchronous function calls the `ocl_sample` function to obtain samples based on the provided operation and chunks. After retrieving the samples, it processes them using the `chunkPointSamplesFromOCL` function. This is useful for handling large datasets in a chunked manner, allowing for efficient sampling and processing. Args: operation (str): The operation to be performed on the chunks. chunks (list): A list of data chunks to be processed. Returns: None: This function does not return a value. """ samples = await ocl_sample(operation, chunks) chunkPointSamplesFromOCL(chunks, samples)
[docs] def oclWaterlineLayerHeights(operation): """Generate a list of waterline layer heights for a given operation. This function calculates the heights of waterline layers based on the specified parameters of the operation. It starts from the maximum height and decrements by a specified step until it reaches the minimum height. The resulting list of heights can be used for further processing in operations that require layered depth information. Args: operation (object): An object containing the properties `minz`, `maxz`, and `stepdown` which define the minimum height, maximum height, and step size for layer generation, respectively. Returns: list: A list of waterline layer heights from maximum to minimum. """ layers = [] l_last = operation.min_z l_step = operation.stepdown l_first = operation.max_z - l_step l_depth = l_first while l_depth > (l_last + 0.0000001): layers.append(l_depth) l_depth -= l_step layers.append(l_last) return layers
[docs] def get_oclSTL(operation): """Get the oclSTL representation from the provided operation. This function iterates through the objects in the given operation and constructs an oclSTL object by extracting triangle data from mesh, curve, font, or surface objects. It activates each object and checks its type to determine if it can be processed. If no valid objects are found, it raises an exception. Args: operation (Operation): An object containing a collection of objects Returns: ocl.STLSurf: An oclSTL object containing the triangles derived from the valid objects. Raises: CamException: If no mesh, curve, or equivalent object is found in """ me = None oclSTL = ocl.STLSurf() found_mesh = False for collision_object in operation.objects: activate(collision_object) if ( collision_object.type == "MESH" or collision_object.type == "CURVE" or collision_object.type == "FONT" or collision_object.type == "SURFACE" ): found_mesh = True global_matrix = mathutils.Matrix.Identity(4) faces = blender_utils.faces_from_mesh( collision_object, global_matrix, operation.use_modifiers ) for face in faces: t = ocl.Triangle( ocl.Point( face[0][0] * OCL_SCALE, face[0][1] * OCL_SCALE, (face[0][2] + operation.skin) * OCL_SCALE, ), ocl.Point( face[1][0] * OCL_SCALE, face[1][1] * OCL_SCALE, (face[1][2] + operation.skin) * OCL_SCALE, ), ocl.Point( face[2][0] * OCL_SCALE, face[2][1] * OCL_SCALE, (face[2][2] + operation.skin) * OCL_SCALE, ), ) oclSTL.addTriangle(t) # FIXME needs to work with collections if not found_mesh: raise CamException( "This Operation Requires a Mesh or Curve Object or Equivalent (e.g. Text, Volume)." ) return oclSTL
[docs] async def ocl_sample(operation, chunks, use_cached_mesh=False): """Sample points using a specified cutter and operation. This function takes an operation and a list of chunks, and samples points based on the specified cutter type and its parameters. It supports various cutter types such as 'END', 'BALLNOSE', 'VCARVE', 'CYLCONE', 'BALLCONE', and 'BULLNOSE'. The function can also utilize a cached mesh for efficiency. The sampled points are returned after processing all chunks. Args: operation (Operation): An object containing the cutter type, diameter, minimum Z value, tip angle, and other relevant parameters. chunks (list): A list of chunk objects that contain point data to be processed. use_cached_mesh (bool): A flag indicating whether to use a cached mesh if available. Defaults to False. Returns: list: A list of sampled CL points generated by the cutter. """ global _PREVIOUS_OCL_MESH op_cutter_type = operation.cutter_type op_cutter_diameter = operation.cutter_diameter op_minz = operation.min_z op_cutter_tip_angle = radians(operation.cutter_tip_angle) / 2 if op_cutter_type == "VCARVE": cutter_length = (op_cutter_diameter / tan(op_cutter_tip_angle)) / 2 else: cutter_length = 10 cutter = None if op_cutter_type == "END": cutter = ocl.CylCutter((op_cutter_diameter + operation.skin * 2) * 1000, cutter_length) elif op_cutter_type == "BALLNOSE": cutter = ocl.BallCutter((op_cutter_diameter + operation.skin * 2) * 1000, cutter_length) elif op_cutter_type == "VCARVE": cutter = ocl.ConeCutter( (op_cutter_diameter + operation.skin * 2) * 1000, op_cutter_tip_angle, cutter_length ) elif op_cutter_type == "CYLCONE": cutter = ocl.CylConeCutter( (operation.cylcone_diameter / 2 + operation.skin) * 2000, (op_cutter_diameter + operation.skin * 2) * 1000, op_cutter_tip_angle, ) elif op_cutter_type == "BALLCONE": cutter = ocl.BallConeCutter( (operation.ball_radius + operation.skin) * 2000, (op_cutter_diameter + operation.skin * 2) * 1000, op_cutter_tip_angle, ) elif op_cutter_type == "BULLNOSE": cutter = ocl.BullCutter( (op_cutter_diameter + operation.skin * 2) * 1000, operation.bull_corner_radius * 1000, cutter_length, ) else: print("Cutter Unsupported: {0}\n".format(op_cutter_type)) quit() bdc = ocl.BatchDropCutter() if use_cached_mesh and _PREVIOUS_OCL_MESH is not None: oclSTL = _PREVIOUS_OCL_MESH else: oclSTL = get_oclSTL(operation) _PREVIOUS_OCL_MESH = oclSTL bdc.setSTL(oclSTL) bdc.setCutter(cutter) for chunk in chunks: for coord in chunk.get_points_np(): bdc.appendPoint(ocl.CLPoint(coord[0] * 1000, coord[1] * 1000, op_minz * 1000)) await progress_async("OpenCAMLib Sampling") bdc.run() cl_points = bdc.getCLPoints() return cl_points