"""Fabex 'operation_utils.py' © 2012 Vilem Novak
Main functionality of Fabex.
The functions here are called with operators defined in 'ops.py'
"""
from math import pi
from pathlib import Path
import pickle
import bpy
from bpy_extras import object_utils
from .simple_utils import get_cache_path
from .simple_utils import unit_value_to_string
from ..constants import was_hidden_dict
[docs]
def get_operation_sources(o):
"""Get operation sources based on the geometry source type.
This function retrieves and sets the operation sources for a given
object based on its geometry source type. It handles three types of
geometry sources: 'OBJECT', 'COLLECTION', and 'IMAGE'. For 'OBJECT', it
selects the specified object and applies rotations if enabled. For
'COLLECTION', it retrieves all objects within the specified collection.
For 'IMAGE', it sets a specific optimization flag. Additionally, it
determines whether the objects are curves or meshes based on the
geometry source.
Args:
o (Object): An object containing properties such as geometry_source,
object_name, collection_name, rotation_a, rotation_b,
enable_A, enable_B, old_rotation_a, old_rotation_b,
A_along_x, and optimisation.
Returns:
None: This function does not return a value but modifies the
properties of the input object.
"""
if o.geometry_source == "OBJECT":
# bpy.ops.object.select_all(action='DESELECT')
ob = bpy.data.objects[o.object_name]
o.objects = [ob]
ob.select_set(True)
bpy.context.view_layer.objects.active = ob
if o.enable_b_axis or o.enable_a_axis:
if o.old_rotation_a != o.rotation_a or o.old_rotation_b != o.rotation_b:
o.old_rotation_a = o.rotation_a
o.old_rotation_b = o.rotation_b
ob = bpy.data.objects[o.object_name]
ob.select_set(True)
bpy.context.view_layer.objects.active = ob
if o.a_along_x: # A parallel with X
if o.enable_a_axis:
bpy.context.active_object.rotation_euler.x = o.rotation_a
if o.enable_b_axis:
bpy.context.active_object.rotation_euler.y = o.rotation_b
else: # A parallel with Y
if o.enable_a_axis:
bpy.context.active_object.rotation_euler.y = o.rotation_a
if o.enable_b_axis:
bpy.context.active_object.rotation_euler.x = o.rotation_b
elif o.geometry_source == "COLLECTION":
collection = bpy.data.collections[o.collection_name]
o.objects = collection.objects
elif o.geometry_source == "IMAGE":
o.optimisation.use_exact = False
if o.geometry_source == "OBJECT" or o.geometry_source == "COLLECTION":
o.onlycurves = True
for ob in o.objects:
if ob.type == "MESH":
o.onlycurves = False
else:
o.onlycurves = False
[docs]
def reload_paths(o):
"""Reload the camera path data from a pickle file.
This function retrieves the camera path data associated with the given
object `o`. It constructs a new mesh from the path vertices and updates
the object's properties with the loaded data. If a previous path mesh
exists, it is removed to avoid memory leaks. The function also handles
the creation of a new mesh object if one does not already exist in the
current scene.
Args:
o (Object): The object for which the camera path is being
"""
oname = "cam_path_" + o.name
s = bpy.context.scene
# for o in s.objects:
ob = None
old_pathmesh = None
if oname in s.objects:
old_pathmesh = s.objects[oname].data
ob = s.objects[oname]
picklepath = get_cache_path(o) + ".pickle"
f = open(picklepath, "rb")
d = pickle.load(f)
f.close()
o.info.warnings = d["warnings"]
o.info.duration = d["duration"]
verts = d["path"]
edges = []
for a in range(0, len(verts) - 1):
edges.append((a, a + 1))
oname = "cam_path_" + 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
else:
object_utils.object_data_add(bpy.context, mesh, operator=None)
ob = bpy.context.active_object
ob.name = oname
ob = s.objects[oname]
ob.location = (0, 0, 0)
o.path_object_name = oname
o.changed = False
if old_pathmesh is not None:
bpy.data.meshes.remove(old_pathmesh)
def update_operation(self, context):
"""Update the visibility and selection state of camera operations in the
scene.
This method manages the visibility of objects associated with camera
operations based on the current active operation. If the
'hide_all_others' flag is set to true, it hides all other objects except
for the currently active one. If the flag is false, it restores the
visibility of previously hidden objects. The method also attempts to
highlight the currently active object in the 3D view and make it the
active object in the scene.
Args:
context (bpy.types.Context): The context containing the current scene and
"""
scene = context.scene
ao = scene.cam_operations[scene.cam_active_operation]
operation_valid(self, context)
if ao.hide_all_others:
for _ao in scene.cam_operations:
if _ao.path_object_name in bpy.data.objects:
other_obj = bpy.data.objects[_ao.path_object_name]
current_obj = bpy.data.objects[ao.path_object_name]
if other_obj != current_obj:
other_obj.hide = True
other_obj.select = False
else:
for path_obj_name in was_hidden_dict:
print(was_hidden_dict)
if was_hidden_dict[path_obj_name]:
# Find object and make it hidde, then reset 'hidden' flag
obj = bpy.data.objects[path_obj_name]
obj.hide = True
obj.select = False
was_hidden_dict[path_obj_name] = False
# try highlighting the object in the 3d view and make it active
bpy.ops.object.select_all(action="DESELECT")
# highlight the cutting path if it exists
try:
ob = bpy.data.objects[ao.path_object_name]
ob.select_set(state=True, view_layer=None)
# Show object if, it's was hidden
if ob.hide:
ob.hide = False
was_hidden_dict[ao.path_object_name] = True
bpy.context.scene.objects.active = ob
except Exception as e:
print(e)
[docs]
def source_valid(o, context):
"""Check the validity of a geometry source.
This function verifies if the provided geometry source is valid based on
its type. It checks for three types of geometry sources: 'OBJECT',
'COLLECTION', and 'IMAGE'. For 'OBJECT', it ensures that the object name
ends with '_cut_bridges' or exists in the Blender data objects. For
'COLLECTION', it checks if the collection name exists and contains
objects. For 'IMAGE', it verifies if the source image name exists in the
Blender data images.
Args:
o (object): An object containing geometry source information, including
attributes like `geometry_source`, `object_name`, `collection_name`,
and `source_image_name`.
context: The context in which the validation is performed (not used in this
function).
Returns:
bool: True if the geometry source is valid, False otherwise.
"""
valid = True
if o.geometry_source == "OBJECT":
if not o.object_name.endswith("_cut_bridges"): # let empty bridge cut be valid
if o.object_name not in bpy.data.objects:
valid = False
if o.geometry_source == "COLLECTION":
if o.collection_name not in bpy.data.collections:
valid = False
elif len(bpy.data.collections[o.collection_name].objects) == 0:
valid = False
if o.geometry_source == "IMAGE":
if o.source_image_name not in bpy.data.images:
valid = False
return valid
[docs]
def operation_valid(self, context):
"""Validate the current camera operation in the given context.
This method checks if the active camera operation is valid based on the
current scene context. It updates the operation's validity status and
provides warnings if the source object is invalid. Additionally, it
configures specific settings related to image geometry sources.
Args:
context (Context): The context containing the scene and camera operations.
"""
scene = context.scene
o = scene.cam_operations[scene.cam_active_operation]
o.changed = True
o.valid = source_valid(o, context)
invalidmsg = "Invalid Source Object for Operation.\n"
if o.valid:
o.info.warnings = ""
else:
o.info.warnings = invalidmsg
addon_prefs = bpy.context.preferences.addons["bl_ext.user_default.fabex"].preferences
if addon_prefs.show_popups:
bpy.ops.cam.popup("INVOKE_DEFAULT")
if o.geometry_source == "IMAGE":
o.optimisation.use_exact = False
o.update_offset_image_tag = True
o.update_z_buffer_image_tag = True
print("Validity ")
[docs]
def chain_valid(chain, context):
"""Check the validity of a chain of operations within a given context.
This function verifies if all operations in the provided chain are valid
according to the current scene context. It first checks if the chain
contains any operations. If it does, it iterates through each operation
in the chain and checks if it exists in the scene's camera operations.
If an operation is not found or is deemed invalid, the function returns
a tuple indicating the failure and provides an appropriate error
message. If all operations are valid, it returns a success indication.
Args:
chain (Chain): The chain of operations to validate.
context (Context): The context containing the scene and camera operations.
Returns:
tuple: A tuple containing a boolean indicating validity and an error message
(if any). The first element is True if valid, otherwise False. The
second element is an error message string.
"""
s = context.scene
if len(chain.operations) == 0:
return (False, "")
for cho in chain.operations:
found_op = None
for so in s.cam_operations:
if so.name == cho.name:
found_op = so
if found_op == None:
return (False, f"Couldn't Find Operation {cho.name}")
if source_valid(found_op, context) is False:
return (False, f"Operation {found_op.name} Is Not Valid")
return (True, "")
[docs]
def update_operation_valid(self, context):
update_operation(self, context)
# Update functions start here
[docs]
def update_chipload(self, context):
"""Update the chipload based on feedrate, spindle RPM, and cutter
parameters.
This function calculates the chipload using the formula: chipload =
feedrate / (spindle_rpm * cutter_flutes). It also attempts to account
for chip thinning when cutting at less than 50% cutter engagement with
cylindrical end mills by combining two formulas. The first formula
provides the nominal chipload based on standard recommendations, while
the second formula adjusts for the cutter diameter and distance between
paths. The current implementation may not yield consistent results, and
there are concerns regarding the correctness of the units used in the
calculations. Further review and refinement of this function may be
necessary to improve accuracy and reliability.
Args:
context: The context in which the update is performed (not used in this
implementation).
Returns:
None: This function does not return a value; it updates the chipload in place.
"""
print("Update Chipload ")
o = self
# Old chipload
o.info.chipload = o.feedrate / (o.spindle_rpm * o.cutter_flutes)
o.info.chipload_per_tooth = unit_value_to_string(o.info.chipload, 4)
# New chipload with chip thining compensation.
# I have tried to combine these 2 formulas to compinsate for the phenomenon of chip thinning when cutting at less
# than 50% cutter engagement with cylindrical end mills. formula 1 Nominal Chipload is
# " feedrate mm/minute = spindle rpm x chipload x cutter diameter mm x cutter_flutes "
# formula 2 (.5*(cutter diameter mm devided by distance_between_paths)) divided by square root of
# ((cutter diameter mm devided by distance_between_paths)-1) x Nominal Chipload
# Nominal Chipload = what you find in end mill data sheats recomended chip load at %50 cutter engagment.
# I am sure there is a better way to do this. I dont get consistent result and
# I am not sure if there is something wrong with the units going into the formula, my math or my lack of
# underestanding of python or programming in genereal. Hopefuly some one can have a look at this and with any luck
# we will be one tiny step on the way to a slightly better chipload calculating function.
# self.chipload = ((0.5*(o.cutter_diameter/o.distance_between_paths))/(sqrt((o.feedrate*1000)/(o.spindle_rpm*o.cutter_diameter*o.cutter_flutes)*(o.cutter_diameter/o.distance_between_paths)-1)))
print(f"Chipload: {o.info.chipload}")
print(f"Chipload per Tooth: {o.info.chipload_per_tooth}")
[docs]
def update_offset_image(self, context):
"""Refresh the Offset Image Tag for re-rendering.
This method updates the chip load and marks the offset image tag for re-
rendering. It sets the `changed` attribute to True and indicates that
the offset image tag needs to be updated.
Args:
context: The context in which the update is performed.
"""
update_chipload(self, context)
print("Update Offset")
self.changed = True
self.update_offset_image_tag = True
[docs]
def update_Z_buffer_image(self, context):
"""Update the Z-buffer and offset image tags for recalculation.
This method modifies the internal state to indicate that the Z-buffer
image and offset image tags need to be updated during the calculation
process. It sets the `changed` attribute to True and marks the relevant
tags for updating. Additionally, it calls the `getOperationSources`
function to ensure that the necessary operation sources are retrieved.
Args:
context: The context in which the update is being performed.
"""
self.changed = True
self.update_z_buffer_image_tag = True
self.update_offset_image_tag = True
get_operation_sources(self)
[docs]
def update_image_size_y(self, context):
"""Updates the Image Y size based on the following function."""
if self.source_image_name != "":
i = bpy.data.images[self.source_image_name]
if i is not None:
size_x = self.source_image_size_x / i.size[0]
size_y = int(x_size * i.size[1] * 1000000) / 1000
col.label(text="Image Size on Y Axis: " + unit_value_to_string(size_y, 8))
col.separator()
[docs]
def update_bridges(o, context):
"""Update the status of bridges.
This function marks the bridge object as changed, indicating that an
update has occurred. It prints a message to the console for logging
purposes. The function takes in an object and a context, but the context
is not utilized within the function.
Args:
o (object): The bridge object that needs to be updated.
context (object): Additional context for the update, not used in this function.
"""
print("Update Bridges ")
o.changed = True
[docs]
def update_rotation(o, context):
"""Update the rotation of a specified object in Blender.
This function modifies the rotation of a Blender object based on the
properties of the provided object 'o'. It checks which rotations are
enabled and applies the corresponding rotation values to the active
object in the scene. The rotation can be aligned either along the X or Y
axis, depending on the configuration of 'o'.
Args:
o (object): An object containing rotation settings and flags.
context (object): The context in which the operation is performed.
"""
print("Update Rotation")
if o.enable_b_axis or o.enable_a_axis:
print(o, o.rotation_a)
ob = bpy.data.objects[o.object_name]
ob.select_set(True)
bpy.context.view_layer.objects.active = ob
if o.a_along_x: # A parallel with X
if o.enable_a_axis:
bpy.context.active_object.rotation_euler.x = o.rotation_a
if o.enable_b_axis:
bpy.context.active_object.rotation_euler.y = o.rotation_b
else: # A parallel with Y
if o.enable_a_axis:
bpy.context.active_object.rotation_euler.y = o.rotation_a
if o.enable_b_axis:
bpy.context.active_object.rotation_euler.x = o.rotation_b
[docs]
def update_rest(o, context):
"""Update the state of the object.
This function modifies the given object by setting its 'changed'
attribute to True. It also prints a message indicating that the update
operation has been performed.
Args:
o (object): The object to be updated.
context (object): The context in which the update is being performed.
"""
print("Update Rest ")
o.changed = True
[docs]
def update_operation(self, context):
"""Update the camera operation based on the current context.
This function retrieves the active camera operation from the Blender
context and updates it using the `updateRest` function. It accesses the
active operation from the scene's camera operations and passes the
current context to the updating function.
Args:
context: The context in which the operation is being updated.
"""
# from . import updateRest
active_op = bpy.context.scene.cam_operations[bpy.context.scene.cam_active_operation]
update_rest(active_op, bpy.context)
[docs]
def update_zbuffer_image(self, context):
"""Update the Z-buffer image based on the active camera operation.
This function retrieves the currently active camera operation from the
Blender context and updates the Z-buffer image accordingly. It accesses
the scene's camera operations and invokes the `updateZbufferImage`
function with the active operation and context.
Args:
context (bpy.context): The current Blender context.
"""
# from . import updateZbufferImage
active_op = bpy.context.scene.cam_operations[bpy.context.scene.cam_active_operation]
update_Z_buffer_image(active_op, bpy.context)
[docs]
def get_chain_operations(chain):
"""Return chain operations associated with a given chain object.
This function iterates through the operations of the provided chain
object and retrieves the corresponding operations from the current
scene's camera operations in Blender. Due to limitations in Blender,
chain objects cannot store operations directly, so this function serves
to extract and return the relevant operations for further processing.
Args:
chain (object): The chain object from which to retrieve operations.
Returns:
list: A list of operations associated with the given chain object.
"""
chop = []
for cho in chain.operations:
for so in bpy.context.scene.cam_operations:
if so.name == cho.name:
chop.append(so)
return chop