Source code for cam.simple

"""BlenderCAM 'simple.py' © 2012 Vilem Novak

Various helper functions, less complex than those found in the 'utils' files.
"""

from math import (
    hypot,
    pi,
)
import os
import string
import sys
import time

from shapely.geometry import Polygon

import bpy
from mathutils import Vector

from .constants import BULLET_SCALE


[docs] def tuple_add(t, t1): # add two tuples as Vectors """Add two tuples as vectors. This function takes two tuples, each representing a vector in three- dimensional space, and returns a new tuple that is the element-wise sum of the two input tuples. It assumes that both tuples contain exactly three numeric elements. Args: t (tuple): A tuple containing three numeric values representing the first vector. t1 (tuple): A tuple containing three numeric values representing the second vector. Returns: tuple: A tuple containing three numeric values that represent the sum of the input vectors. """ return t[0] + t1[0], t[1] + t1[1], t[2] + t1[2]
[docs] def tuple_sub(t, t1): # sub two tuples as Vectors """Subtract two tuples element-wise. This function takes two tuples of three elements each and performs an element-wise subtraction, treating the tuples as vectors. The result is a new tuple containing the differences of the corresponding elements from the input tuples. Args: t (tuple): A tuple containing three numeric values. t1 (tuple): A tuple containing three numeric values. Returns: tuple: A tuple containing the results of the element-wise subtraction. """ return t[0] - t1[0], t[1] - t1[1], t[2] - t1[2]
[docs] def tuple_mul(t, c): # multiply two tuples with a number """Multiply each element of a tuple by a given number. This function takes a tuple containing three elements and a numeric value, then multiplies each element of the tuple by the provided number. The result is returned as a new tuple containing the multiplied values. Args: t (tuple): A tuple containing three numeric values. c (numeric): A number by which to multiply each element of the tuple. Returns: tuple: A new tuple containing the results of the multiplication. """ return t[0] * c, t[1] * c, t[2] * c
[docs] def tuple_length(t): # get length of vector, but passed in as tuple. """Get the length of a vector represented as a tuple. This function takes a tuple as input, which represents the coordinates of a vector, and returns its length by creating a Vector object from the tuple. The length is calculated using the appropriate mathematical formula for vector length. Args: t (tuple): A tuple representing the coordinates of the vector. Returns: float: The length of the vector. """ return Vector(t).length
# timing functions for optimisation purposes...
[docs] def timinginit(): """Initialize timing metrics. This function sets up the initial state for timing functions by returning a list containing two zero values. These values can be used to track elapsed time or other timing-related metrics in subsequent operations. Returns: list: A list containing two zero values, representing the initial timing metrics. """ return [0, 0]
[docs] def timingstart(tinf): """Start timing by recording the current time. This function updates the second element of the provided list with the current time in seconds since the epoch. It is useful for tracking the start time of an operation or process. Args: tinf (list): A list where the second element will be updated with the current time. """ t = time.time() tinf[1] = t
[docs] def timingadd(tinf): """Update the timing information. This function updates the first element of the `tinf` list by adding the difference between the current time and the second element of the list. It is typically used to track elapsed time in a timing context. Args: tinf (list): A list where the first element is updated with the """ t = time.time() tinf[0] += t - tinf[1]
[docs] def timingprint(tinf): """Print the timing information. This function takes a tuple containing timing information and prints it in a formatted string. It specifically extracts the first element of the tuple, which is expected to represent time, and appends the string 'seconds' to it before printing. Args: tinf (tuple): A tuple where the first element is expected to be a numeric value representing time. Returns: None: This function does not return any value; it only prints output to the console. """ print('time ' + str(tinf[0]) + 'seconds')
[docs] def progress(text, n=None): """Report progress during script execution. This function outputs a progress message to the standard output. It is designed to work for background operations and provides a formatted string that includes the specified text and an optional numeric progress value. If the numeric value is provided, it is formatted as a percentage. Args: text (str): The message to display as progress. n (float?): A float representing the progress as a fraction (0.0 to 1.0). If not provided, no percentage will be displayed. Returns: None: This function does not return a value; it only prints to the standard output. """ text = str(text) if n is None: n = '' else: n = ' ' + str(int(n * 1000) / 1000) + '%' sys.stdout.write('progress{%s%s}\n' % (text, n)) sys.stdout.flush()
[docs] def activate(o): """Makes an object active in Blender. This function sets the specified object as the active object in the current Blender scene. It first deselects all objects, then selects the given object and makes it the active object in the view layer. This is useful for operations that require a specific object to be active, such as transformations or modifications. Args: o (bpy.types.Object): The Blender object to be activated. """ s = bpy.context.scene bpy.ops.object.select_all(action='DESELECT') o.select_set(state=True) s.objects[o.name].select_set(state=True) bpy.context.view_layer.objects.active = o
[docs] def dist2d(v1, v2): """Calculate the distance between two points in 2D space. This function computes the Euclidean distance between two points represented by their coordinates in a 2D plane. It uses the Pythagorean theorem to calculate the distance based on the differences in the x and y coordinates of the points. Args: v1 (tuple): A tuple representing the coordinates of the first point (x1, y1). v2 (tuple): A tuple representing the coordinates of the second point (x2, y2). Returns: float: The Euclidean distance between the two points. """ return hypot((v1[0] - v2[0]), (v1[1] - v2[1]))
[docs] def delob(ob): """Delete an object in Blender for multiple uses. This function activates the specified object and then deletes it using Blender's built-in operations. It is designed to facilitate the deletion of objects within the Blender environment, ensuring that the object is active before performing the deletion operation. Args: ob (Object): The Blender object to be deleted. """ activate(ob) bpy.ops.object.delete(use_global=False)
[docs] def dupliob(o, pos): """Helper function for visualizing cutter positions in bullet simulation. This function duplicates the specified object and resizes it according to a predefined scale factor. It also removes any existing rigidbody properties from the duplicated object and sets its location to the specified position. This is useful for managing multiple cutter positions in a bullet simulation environment. Args: o (Object): The object to be duplicated. pos (Vector): The new position to place the duplicated object. """ activate(o) bpy.ops.object.duplicate() s = 1.0 / BULLET_SCALE bpy.ops.transform.resize(value=(s, s, s), constraint_axis=(False, False, False), orient_type='GLOBAL', mirror=False, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1) o = bpy.context.active_object bpy.ops.rigidbody.object_remove() o.location = pos
[docs] def addToGroup(ob, groupname): """Add an object to a specified group in Blender. This function activates the given object and checks if the specified group exists in Blender's data. If the group does not exist, it creates a new group with the provided name. If the group already exists, it links the object to that group. Args: ob (Object): The object to be added to the group. groupname (str): The name of the group to which the object will be added. """ activate(ob) if bpy.data.groups.get(groupname) is None: bpy.ops.group.create(name=groupname) else: bpy.ops.object.group_link(group=groupname)
[docs] def compare(v1, v2, vmiddle, e): """Comparison for optimization of paths. This function compares two vectors and checks if the distance between a calculated vector and a reference vector is less than a specified threshold. It normalizes the vector difference and scales it by the length of another vector to determine if the resulting vector is within the specified epsilon value. Args: v1 (Vector): The first vector for comparison. v2 (Vector): The second vector for comparison. vmiddle (Vector): The middle vector used for calculating the reference vector. e (float): The threshold value for comparison. Returns: bool: True if the distance is less than the threshold, otherwise False. """ # e=0.0001 v1 = Vector(v1) v2 = Vector(v2) vmiddle = Vector(vmiddle) vect1 = v2 - v1 vect2 = vmiddle - v1 vect1.normalize() vect1 *= vect2.length v = vect2 - vect1 if v.length < e: return True return False
[docs] def isVerticalLimit(v1, v2, limit): """Test Path Segment on Verticality Threshold for protect_vertical option. This function evaluates the verticality of a path segment defined by two points, v1 and v2, based on a specified limit. It calculates the angle between the vertical vector and the vector formed by the two points. If the angle is within the defined limit, it adjusts the vertical position of either v1 or v2 to ensure that the segment adheres to the verticality threshold. Args: v1 (tuple): A 3D point represented as a tuple (x, y, z). v2 (tuple): A 3D point represented as a tuple (x, y, z). limit (float): The angle threshold for determining verticality. Returns: tuple: The adjusted 3D points v1 and v2 after evaluating the verticality. """ z = abs(v1[2] - v2[2]) # verticality=0.05 # this will be better. # # print(a) if z > 0: v2d = Vector((0, 0, -1)) v3d = Vector((v1[0] - v2[0], v1[1] - v2[1], v1[2] - v2[2])) a = v3d.angle(v2d) if a > pi / 2: a = abs(a - pi) # print(a) if a < limit: # print(abs(v1[0]-v2[0])/z) # print(abs(v1[1]-v2[1])/z) if v1[2] > v2[2]: v1 = (v2[0], v2[1], v1[2]) return v1, v2 else: v2 = (v1[0], v1[1], v2[2]) return v1, v2 return v1, v2
[docs] def getCachePath(o): """Get the cache path for a given object. This function constructs a cache path based on the current Blender file's filepath and the name of the provided object. It retrieves the base name of the file, removes the last six characters, and appends a specified directory and the object's name to create a complete cache path. Args: o (Object): The Blender object for which the cache path is being generated. Returns: str: The constructed cache path as a string. """ fn = bpy.data.filepath l = len(bpy.path.basename(fn)) bn = bpy.path.basename(fn)[:-6] print('fn-l:', fn[:-l]) print('bn:', bn) iname = fn[:-l] + 'temp_cam' + os.sep + bn + '_' + o.name return iname
[docs] def getSimulationPath(): """Get the simulation path for temporary camera files. This function retrieves the file path of the current Blender project and constructs a new path for temporary camera files by appending 'temp_cam' to the directory of the current file. The constructed path is returned as a string. Returns: str: The path to the temporary camera directory. """ fn = bpy.data.filepath l = len(bpy.path.basename(fn)) iname = fn[:-l] + 'temp_cam' + os.sep return iname
[docs] def safeFileName(name): # for export gcode """Generate a safe file name from the given string. This function takes a string input and removes any characters that are not considered valid for file names. The valid characters include letters, digits, and a few special characters. The resulting string can be used safely as a file name for exporting purposes. Args: name (str): The input string to be sanitized into a safe file name. Returns: str: A sanitized version of the input string that contains only valid characters for a file name. """ valid_chars = "-_.()%s%s" % (string.ascii_letters, string.digits) filename = ''.join(c for c in name if c in valid_chars) return filename
[docs] def strInUnits(x, precision=5): """Convert a value to a string representation in the current unit system. This function takes a numeric value and converts it to a string formatted according to the unit system set in the Blender context. If the unit system is metric, the value is converted to millimeters. If the unit system is imperial, the value is converted to inches. The precision of the output can be specified. Args: x (float): The numeric value to be converted. precision (int?): The number of decimal places to round to. Defaults to 5. Returns: str: The string representation of the value in the appropriate units. """ if bpy.context.scene.unit_settings.system == 'METRIC': return str(round(x * 1000, precision)) + ' mm ' elif bpy.context.scene.unit_settings.system == 'IMPERIAL': return str(round(x * 1000 / 25.4, precision)) + "'' " else: return str(x)
# select multiple object starting with name
[docs] def select_multiple(name): """Select multiple objects in the scene based on their names. This function deselects all objects in the current Blender scene and then selects all objects whose names start with the specified prefix. It iterates through all objects in the scene and checks if their names begin with the given string. If they do, those objects are selected; otherwise, they are deselected. Args: name (str): The prefix used to select objects in the scene. """ scene = bpy.context.scene bpy.ops.object.select_all(action='DESELECT') for ob in scene.objects: # join pocket curve calculations if ob.name.startswith(name): ob.select_set(True) else: ob.select_set(False)
# join multiple objects starting with 'name' renaming final object as 'name'
[docs] def join_multiple(name): """Join multiple objects and rename the final object. This function selects multiple objects in the Blender context, joins them into a single object, and renames the resulting object to the specified name. It is assumed that the objects to be joined are already selected in the Blender interface. Args: name (str): The new name for the joined object. """ select_multiple(name) bpy.ops.object.join() bpy.context.active_object.name = name # rename object
# remove multiple objects starting with 'name'.... useful for fixed name operation
[docs] def remove_multiple(name): """Remove multiple objects from the scene based on their name prefix. This function deselects all objects in the current Blender scene and then iterates through all objects. If an object's name starts with the specified prefix, it selects that object and deletes it from the scene. This is useful for operations that require removing multiple objects with a common naming convention. Args: name (str): The prefix of the object names to be removed. """ scene = bpy.context.scene bpy.ops.object.select_all(action='DESELECT') for ob in scene.objects: if ob.name.startswith(name): ob.select_set(True) bpy.ops.object.delete()
[docs] def deselect(): """Deselect all objects in the current Blender context. This function utilizes the Blender Python API to deselect all objects in the current scene. It is useful for clearing selections before performing other operations on objects. Raises: None """ bpy.ops.object.select_all(action='DESELECT')
# makes the object with the name active
[docs] def make_active(name): """Make an object active in the Blender scene. This function takes the name of an object and sets it as the active object in the current Blender scene. It first deselects all objects, then selects the specified object and makes it active, allowing for further operations to be performed on it. Args: name (str): The name of the object to be made active. """ ob = bpy.context.scene.objects[name] bpy.ops.object.select_all(action='DESELECT') bpy.context.view_layer.objects.active = ob ob.select_set(True)
# change the name of the active object
[docs] def active_name(name): """Change the name of the active object in Blender. This function sets the name of the currently active object in the Blender context to the specified name. It directly modifies the `name` attribute of the active object, allowing users to rename objects programmatically. Args: name (str): The new name to assign to the active object. """ bpy.context.active_object.name = name
# renames and makes active name and makes it active
[docs] def rename(name, name2): """Rename an object and make it active. This function renames an object in the Blender context and sets it as the active object. It first calls the `make_active` function to ensure the object is active, then updates the name of the active object to the new name provided. Args: name (str): The current name of the object to be renamed. name2 (str): The new name to assign to the active object. """ make_active(name) bpy.context.active_object.name = name2
# boolean union of objects starting with name result is object name. # all objects starting with name will be deleted and the result will be name
[docs] def union(name): """Perform a boolean union operation on objects. This function selects multiple objects that start with the given name, performs a boolean union operation on them using Blender's operators, and then renames the resulting object to the specified name. After the operation, it removes the original objects that were used in the union process. Args: name (str): The base name of the objects to be unioned. """ select_multiple(name) bpy.ops.object.curve_boolean(boolean_type='UNION') active_name('unionboolean') remove_multiple(name) rename('unionboolean', name)
[docs] def intersect(name): """Perform an intersection operation on a curve object. This function selects multiple objects based on the provided name and then executes a boolean operation to create an intersection of the selected objects. The resulting intersection is then named accordingly. Args: name (str): The name of the object(s) to be selected for the intersection. """ select_multiple(name) bpy.ops.object.curve_boolean(boolean_type='INTERSECT') active_name('intersection')
# boolean difference of objects starting with name result is object from basename. # all objects starting with name will be deleted and the result will be basename
[docs] def difference(name, basename): """Perform a boolean difference operation on objects. This function selects a series of objects specified by `name` and performs a boolean difference operation with the object specified by `basename`. After the operation, the resulting object is renamed to 'booleandifference'. The original objects specified by `name` are deleted after the operation. Args: name (str): The name of the series of objects to select for the operation. basename (str): The name of the base object to perform the boolean difference with. """ # name is the series to select # basename is what the base you want to cut including name select_multiple(name) bpy.context.view_layer.objects.active = bpy.data.objects[basename] bpy.ops.object.curve_boolean(boolean_type='DIFFERENCE') active_name('booleandifference') remove_multiple(name) rename('booleandifference', basename)
# duplicate active object or duplicate move # if x or y not the default, duplicate move will be executed
[docs] def duplicate(x=0.0, y=0.0): """Duplicate an active object or move it based on the provided coordinates. This function duplicates the currently active object in Blender. If both x and y are set to their default values (0), the object is duplicated in place. If either x or y is non-zero, the object is duplicated and moved by the specified x and y offsets. Args: x (float): The x-coordinate offset for the duplication. Defaults to 0. y (float): The y-coordinate offset for the duplication. Defaults to 0. """ if x == 0.0 and y == 0.0: bpy.ops.object.duplicate() else: bpy.ops.object.duplicate_move(OBJECT_OT_duplicate={"linked": False, "mode": 'TRANSLATION'}, TRANSFORM_OT_translate={"value": (x, y, 0.0)})
# Mirror active object along the x axis
[docs] def mirrorx(): """Mirror the active object along the x-axis. This function utilizes Blender's operator to mirror the currently active object in the 3D view along the x-axis. It sets the orientation to global and applies the transformation based on the specified orientation matrix and constraint axis. """ bpy.ops.transform.mirror(orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', constraint_axis=(True, False, False))
# mirror active object along y axis
[docs] def mirrory(): """Mirror the active object along the Y axis. This function uses Blender's operator to perform a mirror transformation on the currently active object in the scene. The mirroring is done with respect to the global coordinate system, specifically along the Y axis. This can be useful for creating symmetrical objects or for correcting the orientation of an object in a 3D environment. Raises: None """ bpy.ops.transform.mirror(orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', constraint_axis=(False, True, False))
# move active object and apply translation
[docs] def move(x=0.0, y=0.0): """Move the active object in the 3D space by applying a translation. This function translates the active object in Blender's 3D view by the specified x and y values. It uses Blender's built-in operations to perform the translation and then applies the transformation to the object's location. Args: x (float): The distance to move the object along the x-axis. Defaults to 0.0. y (float): The distance to move the object along the y-axis. Defaults to 0.0. """ bpy.ops.transform.translate(value=(x, y, 0.0)) bpy.ops.object.transform_apply(location=True)
# Rotate active object and apply rotation
[docs] def rotate(angle): """Rotate the active object by a specified angle. This function modifies the rotation of the currently active object in the Blender context by setting its Z-axis rotation to the given angle. After updating the rotation, it applies the transformation to ensure that the changes are saved to the object's data. Args: angle (float): The angle in radians to rotate the active object around the Z-axis. """ bpy.context.object.rotation_euler[2] = angle bpy.ops.object.transform_apply(rotation=True)
# remove doubles
[docs] def remove_doubles(): """Remove duplicate vertices from the selected curve object. This function utilizes the Blender Python API to remove duplicate vertices from the currently selected curve object in the Blender environment. It is essential for cleaning up geometry and ensuring that the curve behaves as expected without unnecessary complexity. """ bpy.ops.object.curve_remove_doubles()
# Add overcut to active object
[docs] def add_overcut(diametre, overcut=True): """Add overcut to the active object. This function adds an overcut to the currently active object in the Blender context. If the `overcut` parameter is set to True, it performs a series of operations including creating a curve overcut with the specified diameter, deleting the original object, and renaming the new object to match the original. The function also ensures that any duplicate vertices are removed from the resulting object. Args: diametre (float): The diameter to be used for the overcut. overcut (bool): A flag indicating whether to apply the overcut. Defaults to True. """ if overcut: name = bpy.context.active_object.name bpy.ops.object.curve_overcuts(diameter=diametre, threshold=pi/2.05) overcut_name = bpy.context.active_object.name make_active(name) bpy.ops.object.delete() rename(overcut_name, name) remove_doubles()
# add bounding rectangtle to curve
[docs] def add_bound_rectangle(xmin, ymin, xmax, ymax, name='bounds_rectangle'): """Add a bounding rectangle to a curve. This function creates a rectangle defined by the minimum and maximum x and y coordinates provided as arguments. The rectangle is added to the scene at the center of the defined bounds. The resulting rectangle is named according to the 'name' parameter. Args: xmin (float): The minimum x-coordinate of the rectangle. ymin (float): The minimum y-coordinate of the rectangle. xmax (float): The maximum x-coordinate of the rectangle. ymax (float): The maximum y-coordinate of the rectangle. name (str): The name of the resulting rectangle object. Defaults to 'bounds_rectangle'. """ xsize = xmax - xmin ysize = ymax - ymin bpy.ops.curve.simple(align='WORLD', location=(xmin + xsize/2, ymin + ysize/2, 0), rotation=(0, 0, 0), Simple_Type='Rectangle', Simple_width=xsize, Simple_length=ysize, use_cyclic_u=True, edit_mode=False, shape='3D') bpy.ops.object.transform_apply(location=True) active_name(name)
[docs] def add_rectangle(width, height, center_x=True, center_y=True): """Add a rectangle to the scene. This function creates a rectangle in the 3D space using the specified width and height. The rectangle can be centered at the origin or offset based on the provided parameters. If `center_x` or `center_y` is set to True, the rectangle will be positioned at the center of the specified dimensions; otherwise, it will be positioned based on the offsets. Args: width (float): The width of the rectangle. height (float): The height of the rectangle. center_x (bool?): If True, centers the rectangle along the x-axis. Defaults to True. center_y (bool?): If True, centers the rectangle along the y-axis. Defaults to True. """ x_offset = width / 2 y_offset = height / 2 if center_x: x_offset = 0 if center_y: y_offset = 0 bpy.ops.curve.simple(align='WORLD', location=(x_offset, y_offset, 0), rotation=(0, 0, 0), Simple_Type='Rectangle', Simple_width=width, Simple_length=height, use_cyclic_u=True, edit_mode=False, shape='3D') bpy.ops.object.transform_apply(location=True) active_name('simple_rectangle')
# Returns coords from active object
[docs] def active_to_coords(): """Convert the active object to a list of its vertex coordinates. This function duplicates the currently active object in the Blender context, converts it to a mesh, and extracts the X and Y coordinates of its vertices. After extracting the coordinates, it removes the temporary mesh object created during the process. The resulting list contains tuples of (x, y) coordinates for each vertex in the active object. Returns: list: A list of tuples, each containing the X and Y coordinates of the vertices from the active object. """ bpy.ops.object.duplicate() obj = bpy.context.active_object bpy.ops.object.convert(target='MESH') active_name("_tmp_mesh") coords = [] for v in obj.data.vertices: # extract X,Y coordinates from the vertices data coords.append((v.co.x, v.co.y)) remove_multiple('_tmp_mesh') return coords
# returns shapely polygon from active object
[docs] def active_to_shapely_poly(): """Convert the active object to a Shapely polygon. This function retrieves the coordinates of the currently active object and converts them into a Shapely Polygon data structure. It is useful for geometric operations and spatial analysis using the Shapely library. Returns: Polygon: A Shapely Polygon object created from the active object's coordinates. """ # convert coordinates to shapely Polygon datastructure return Polygon(active_to_coords())
#checks for curve splines shorter than three points and subdivides if necessary
[docs] def subdivide_short_lines(co): """Subdivide all polylines to have at least three points. This function iterates through the splines of a curve, checks if they are not bezier and if they have less or equal to two points. If so, each spline is subdivided to get at least three points. Args: co (Object): A curve object to be analyzed and modified. """ bpy.ops.object.mode_set(mode="EDIT") for sp in co.data.splines: if len(sp.points) == 2 and sp.type != 'BEZIER': bpy.ops.curve.select_all(action='DESELECT') for pt in sp.points: pt.select = True bpy.ops.curve.subdivide() bpy.ops.object.editmode_toggle() bpy.ops.object.select_all(action='SELECT')