Source code for cam.collision

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

Functions for Bullet and Cutter collision checks.
"""

from math import (
    cos,
    pi,
    radians,
    sin,
    tan,
)
import time

import bpy
from mathutils import (
    Euler,
    Vector,
)

from .constants import (
    BULLET_SCALE,
    CUTTER_OFFSET,
)
from .simple import (
    activate,
    delob,
    progress,
)


[docs] def getCutterBullet(o): """Create a cutter for Rigidbody simulation collisions. This function generates a 3D cutter object based on the specified cutter type and parameters. It supports various cutter types including 'END', 'BALLNOSE', 'VCARVE', 'CYLCONE', 'BALLCONE', and 'CUSTOM'. The function also applies rigid body physics to the created cutter for realistic simulation in Blender. Args: o (object): An object containing properties such as cutter_type, cutter_diameter, cutter_tip_angle, ball_radius, and cutter_object_name. Returns: bpy.types.Object: The created cutter object with rigid body properties applied. """ s = bpy.context.scene if s.objects.get('cutter') is not None: c = s.objects['cutter'] activate(c) type = o.cutter_type if type == 'END': bpy.ops.mesh.primitive_cylinder_add(vertices=32, radius=o.cutter_diameter / 2, depth=o.cutter_diameter, end_fill_type='NGON', align='WORLD', enter_editmode=False, location=CUTTER_OFFSET, rotation=(0, 0, 0)) cutter = bpy.context.active_object cutter.scale *= BULLET_SCALE bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) bpy.ops.object.origin_set(type='GEOMETRY_ORIGIN', center='BOUNDS') bpy.ops.rigidbody.object_add(type='ACTIVE') cutter = bpy.context.active_object cutter.rigid_body.collision_shape = 'CYLINDER' elif type == 'BALLNOSE': # ballnose ending used mainly when projecting from sides. # the actual collision shape is capsule in this case. bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=3, radius=o.cutter_diameter / 2, align='WORLD', enter_editmode=False, location=CUTTER_OFFSET, rotation=(0, 0, 0)) cutter = bpy.context.active_object cutter.scale *= BULLET_SCALE bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) bpy.ops.object.origin_set(type='GEOMETRY_ORIGIN', center='BOUNDS') bpy.ops.rigidbody.object_add(type='ACTIVE') cutter = bpy.context.active_object # cutter.dimensions.z = 0.2 * BULLET_SCALE # should be sufficient for now... 20 cm. cutter.rigid_body.collision_shape = 'CAPSULE' #bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) elif type == 'VCARVE': angle = o.cutter_tip_angle s = tan(pi * (90 - angle / 2) / 180) / 2 # angles in degrees cone_d = o.cutter_diameter * s bpy.ops.mesh.primitive_cone_add(vertices=32, radius1=o.cutter_diameter / 2, radius2=0, depth=cone_d, end_fill_type='NGON', align='WORLD', enter_editmode=False, location=CUTTER_OFFSET, rotation=(pi, 0, 0)) cutter = bpy.context.active_object cutter.scale *= BULLET_SCALE bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) bpy.ops.object.origin_set(type='GEOMETRY_ORIGIN', center='BOUNDS') bpy.ops.rigidbody.object_add(type='ACTIVE') cutter = bpy.context.active_object cutter.rigid_body.collision_shape = 'CONE' elif type == 'CYLCONE': angle = o.cutter_tip_angle s = tan(pi * (90 - angle / 2) / 180) / 2 # angles in degrees cylcone_d = (o.cutter_diameter - o.cylcone_diameter) * s bpy.ops.mesh.primitive_cone_add(vertices=32, radius1=o.cutter_diameter / 2, radius2=o.cylcone_diameter / 2, depth=cylcone_d, end_fill_type='NGON', align='WORLD', enter_editmode=False, location=CUTTER_OFFSET, rotation=(pi, 0, 0)) cutter = bpy.context.active_object cutter.scale *= BULLET_SCALE bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) bpy.ops.object.origin_set(type='GEOMETRY_ORIGIN', center='BOUNDS') bpy.ops.rigidbody.object_add(type='ACTIVE') cutter = bpy.context.active_object cutter.rigid_body.collision_shape = 'CONVEX_HULL' cutter.location = CUTTER_OFFSET elif type == 'BALLCONE': angle = radians(o.cutter_tip_angle)/2 cutter_R = o.cutter_diameter/2 Ball_R = o.ball_radius/cos(angle) conedepth = (cutter_R - o.ball_radius)/tan(angle) bpy.ops.curve.simple(align='WORLD', location=(0, 0, 0), rotation=(0, 0, 0), Simple_Type='Point', use_cyclic_u=False) oy = Ball_R for i in range(1, 10): ang = -i * (pi/2-angle) / 9 qx = sin(ang) * oy qy = oy - cos(ang) * oy bpy.ops.curve.vertex_add(location=(qx, qy, 0)) conedepth += qy bpy.ops.curve.vertex_add(location=(-cutter_R, conedepth, 0)) #bpy.ops.curve.vertex_add(location=(0 , conedepth , 0)) bpy.ops.object.editmode_toggle() bpy.ops.object.convert(target='MESH') bpy.ops.transform.rotate(value=-pi / 2, orient_axis='X') bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) ob = bpy.context.active_object ob.name = "BallConeTool" ob_scr = ob.modifiers.new(type='SCREW', name='scr') ob_scr.angle = radians(-360) ob_scr.steps = 32 ob_scr.merge_threshold = 0 ob_scr.use_merge_vertices = True bpy.ops.object.modifier_apply(modifier='scr') bpy.data.objects['BallConeTool'].select_set(True) cutter = bpy.context.active_object cutter.scale *= BULLET_SCALE bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) bpy.ops.object.origin_set(type='GEOMETRY_ORIGIN', center='BOUNDS') bpy.ops.rigidbody.object_add(type='ACTIVE') cutter.location = CUTTER_OFFSET cutter.rigid_body.collision_shape = 'CONVEX_HULL' cutter.location = CUTTER_OFFSET elif type == 'CUSTOM': cutob = bpy.data.objects[o.cutter_object_name] activate(cutob) bpy.ops.object.duplicate() bpy.ops.rigidbody.object_add(type='ACTIVE') cutter = bpy.context.active_object scale = o.cutter_diameter / cutob.dimensions.x cutter.scale *= BULLET_SCALE * scale bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) bpy.ops.object.origin_set(type='GEOMETRY_ORIGIN', center='BOUNDS') # print(cutter.dimensions,scale) bpy.ops.rigidbody.object_add(type='ACTIVE') cutter.rigid_body.collision_shape = 'CONVEX_HULL' cutter.location = CUTTER_OFFSET cutter.name = 'cam_cutter' o.cutter_shape = cutter return cutter
[docs] def subdivideLongEdges(ob, threshold): """Subdivide edges of a mesh object that exceed a specified length. This function iteratively checks the edges of a given mesh object and subdivides those that are longer than a specified threshold. The process involves toggling the edit mode of the object, selecting the long edges, and applying a subdivision operation. The function continues to subdivide until no edges exceed the threshold. Args: ob (bpy.types.Object): The Blender object containing the mesh to be subdivided. threshold (float): The length threshold above which edges will be subdivided. """ print('Subdividing Long Edges') m = ob.data scale = (ob.scale.x + ob.scale.y + ob.scale.z) / 3 subdivides = [] n = 1 iter = 0 while n > 0: n = 0 for i, e in enumerate(m.edges): v1 = m.vertices[e.vertices[0]].co v2 = m.vertices[e.vertices[1]].co vec = v2 - v1 l = vec.length if l * scale > threshold: n += 1 subdivides.append(i) if n > 0: print(len(subdivides)) bpy.ops.object.editmode_toggle() # bpy.ops.mesh.quads_convert_to_tris(quad_method='BEAUTY', ngon_method='BEAUTY') # bpy.ops.mesh.tris_convert_to_quads() bpy.ops.mesh.select_all(action='DESELECT') bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE') bpy.ops.object.editmode_toggle() for i in subdivides: m.edges[i].select = True bpy.ops.object.editmode_toggle() bpy.ops.mesh.subdivide(smoothness=0) if iter == 0: bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.quads_convert_to_tris( quad_method='SHORTEST_DIAGONAL', ngon_method='BEAUTY') bpy.ops.mesh.select_all(action='DESELECT') bpy.ops.object.editmode_toggle() ob.update_from_editmode() iter += 1
# n=0 #
[docs] def prepareBulletCollision(o): """Prepares all objects needed for sampling with Bullet collision. This function sets up the Bullet physics simulation by preparing the specified objects for collision detection. It begins by cleaning up any existing rigid bodies that are not part of the 'machine' object. Then, it duplicates the collision objects, converts them to mesh if they are curves or fonts, and applies necessary modifiers. The function also handles the subdivision of long edges and configures the rigid body properties for each object. Finally, it scales the 'machine' objects to the simulation scale and steps through the simulation frames to ensure that all objects are up to date. Args: o (Object): An object containing properties and settings for """ progress('Preparing Collisions') print(o.name) active_collection = bpy.context.view_layer.active_layer_collection.collection t = time.time() s = bpy.context.scene s.gravity = (0, 0, 0) # cleanup rigidbodies wrongly placed somewhere in the scene for ob in bpy.context.scene.objects: if ob.rigid_body is not None and (bpy.data.objects.find('machine') > -1 and ob.name not in bpy.data.objects['machine'].objects): activate(ob) bpy.ops.rigidbody.object_remove() for collisionob in o.objects: bpy.context.view_layer.objects.active = collisionob collisionob.select_set(state=True) bpy.ops.object.duplicate(linked=False) collisionob = bpy.context.active_object if collisionob.type == 'CURVE' or collisionob.type == 'FONT': # support for curve objects collision if collisionob.type == 'CURVE': odata = collisionob.data.dimensions collisionob.data.dimensions = '2D' bpy.ops.object.convert(target='MESH', keep_original=False) if o.use_modifiers: depsgraph = bpy.context.evaluated_depsgraph_get() mesh_owner = collisionob.evaluated_get(depsgraph) newmesh = mesh_owner.to_mesh() oldmesh = collisionob.data collisionob.modifiers.clear() collisionob.data = bpy.data.meshes.new_from_object(mesh_owner.evaluated_get(depsgraph), preserve_all_data_layers=True, depsgraph=depsgraph) bpy.data.meshes.remove(oldmesh) # subdivide long edges here: if o.optimisation.exact_subdivide_edges: subdivideLongEdges(collisionob, o.cutter_diameter * 2) bpy.ops.rigidbody.object_add(type='ACTIVE') # using active instead of passive because of performance.TODO: check if this works also with 4axis... collisionob.rigid_body.collision_shape = 'MESH' collisionob.rigid_body.kinematic = True # this fixed a serious bug and gave big speedup, rbs could move since they are now active... collisionob.rigid_body.collision_margin = o.skin * BULLET_SCALE bpy.ops.transform.resize(value=(BULLET_SCALE, BULLET_SCALE, BULLET_SCALE), constraint_axis=(False, False, False), orient_type='GLOBAL', mirror=False, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, texture_space=False, release_confirm=False) collisionob.location = collisionob.location * BULLET_SCALE bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) bpy.context.view_layer.objects.active = collisionob if active_collection in collisionob.users_collection: active_collection.objects.unlink(collisionob) getCutterBullet(o) # machine objects scaling up to simulation scale if bpy.data.objects.find('machine') > -1: for ob in bpy.data.objects['machine'].objects: activate(ob) bpy.ops.transform.resize(value=(BULLET_SCALE, BULLET_SCALE, BULLET_SCALE), constraint_axis=(False, False, False), orient_type='GLOBAL', mirror=False, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, texture_space=False, release_confirm=False) ob.location = ob.location * BULLET_SCALE # stepping simulation so that objects are up to date bpy.context.scene.frame_set(0) bpy.context.scene.frame_set(1) bpy.context.scene.frame_set(2) progress(time.time() - t)
[docs] def cleanupBulletCollision(o): """Clean up bullet collision objects in the scene. This function checks for the presence of a 'machine' object in the Blender scene and removes any rigid body objects that are not part of the 'machine'. If the 'machine' object is present, it scales the machine objects up to the simulation scale and adjusts their locations accordingly. Args: o: An object that may be used in the cleanup process (specific usage not detailed). Returns: None: This function does not return a value. """ if bpy.data.objects.find('machine') > -1: machinepresent = True else: machinepresent = False for ob in bpy.context.scene.objects: if ob.rigid_body is not None and not (machinepresent and ob.name in bpy.data.objects['machine'].objects): delob(ob) # machine objects scaling up to simulation scale if machinepresent: for ob in bpy.data.objects['machine'].objects: activate(ob) bpy.ops.transform.resize(value=(1.0 / BULLET_SCALE, 1.0 / BULLET_SCALE, 1.0 / BULLET_SCALE), constraint_axis=(False, False, False), orient_type='GLOBAL', mirror=False, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, texture_space=False, release_confirm=False) ob.location = ob.location / BULLET_SCALE
[docs] def getSampleBullet(cutter, x, y, radius, startz, endz): """Perform a collision test for a 3-axis milling cutter. This function simplifies the collision detection process compared to a full 3D test. It utilizes the Blender Python API to perform a convex sweep test on the cutter's position within a specified 3D space. The function checks for collisions between the cutter and other objects in the scene, adjusting for the cutter's radius to determine the effective position of the cutter tip. Args: cutter (object): The milling cutter object used for the collision test. x (float): The x-coordinate of the cutter's position. y (float): The y-coordinate of the cutter's position. radius (float): The radius of the cutter, used to adjust the collision detection. startz (float): The starting z-coordinate for the collision test. endz (float): The ending z-coordinate for the collision test. Returns: float: The adjusted z-coordinate of the cutter tip if a collision is detected; otherwise, returns a value 10 units below the specified endz. """ scene = bpy.context.scene pos = scene.rigidbody_world.convex_sweep_test(cutter, (x * BULLET_SCALE, y * BULLET_SCALE, startz * BULLET_SCALE), (x * BULLET_SCALE, y * BULLET_SCALE, endz * BULLET_SCALE)) # radius is subtracted because we are interested in cutter tip position, this gets collision object center if pos[3] == 1: return (pos[0][2] - radius) / BULLET_SCALE else: return endz - 10
[docs] def getSampleBulletNAxis(cutter, startpoint, endpoint, rotation, cutter_compensation): """Perform a fully 3D collision test for N-Axis milling. This function computes the collision detection between a cutter and a specified path in a 3D space. It takes into account the cutter's rotation and compensation to accurately determine if a collision occurs during the milling process. The function uses Bullet physics for the collision detection and returns the adjusted position of the cutter if a collision is detected. Args: cutter (object): The cutter object used in the milling operation. startpoint (Vector): The starting point of the milling path. endpoint (Vector): The ending point of the milling path. rotation (Euler): The rotation applied to the cutter. cutter_compensation (float): The compensation factor for the cutter's position. Returns: Vector or None: The adjusted position of the cutter if a collision is detected; otherwise, returns None. """ cutterVec = Vector((0, 0, 1)) * cutter_compensation # cutter compensation vector - cutter physics object has center in the middle, while cam needs the tip position. cutterVec.rotate(Euler(rotation)) start = (startpoint * BULLET_SCALE + cutterVec).to_tuple() end = (endpoint * BULLET_SCALE + cutterVec).to_tuple() pos = bpy.context.scene.rigidbody_world.convex_sweep_test(cutter, start, end) if pos[3] == 1: pos = Vector(pos[0]) # rescale and compensate from center to tip. res = pos / BULLET_SCALE - cutterVec / BULLET_SCALE return res else: return None