import bpy import mathutils import bpy_extras from bpy_extras import view3d_utils from .paintModifier import apply_color from .constants import image_name, image_width, image_height class vPainterOperator(bpy.types.Operator): """Set brush color based on unmodified surface normal""" bl_idname = "paint.virtual_painter" bl_label = "Normal-Based Paint Color" bl_options = {'REGISTER', 'UNDO'} # normal_color = bpy.context.scene.normal_color_global normal_color = bpy.props.FloatVectorProperty(size=3, default=(1.0, 1.0, 1.0)) _timer = None # Timer for modal operator def invoke(self, context, event): if context.area.type == 'VIEW_3D': # create image buffer if (image_name in bpy.data.images): img = bpy.data.images[image_name] else: img = bpy.data.images.new(name=image_name, width=image_width, height=image_height, alpha=True) # set buffer active for painting bpy.context.tool_settings.image_paint.canvas = img # add brush callback bpy.app.handlers.depsgraph_update_post.append(brushstroke_callback) print("callback added") # start timer self._timer = context.window_manager.event_timer_add(0.1, window=context.window) # start modal context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} else: self.report({'WARNING'}, "Please run this operator in the 3D View.") return {'CANCELLED'} def modal(self, context, event): # vPainter disabled? if not context.scene.vPainter_settings.is_vPainter_enabled: # remove image buffer img = bpy.data.images.get(image_name) bpy.data.images.remove(img) # remove brush callback if brushstroke_callback in bpy.app.handlers.depsgraph_update_post: print("callback removed") bpy.app.handlers.depsgraph_update_post.remove(brushstroke_callback) # stop timer context.window_manager.event_timer_remove(self._timer) return {'FINISHED'} if context.area: context.area.tag_redraw() # Stroke starts? if event.type == 'LEFTMOUSE' and event.value == 'PRESS' and context.area and context.area.type == 'VIEW_3D': print("painting begon") self.normal_color = self.get_normal_color(context, event) context.scene.normal_color_global = self.normal_color print("color retrieved") return {'PASS_THROUGH'} def get_normal_color(self, context, event): """Compute the normal at the painting location and convert it to a color.""" rv3d = context.space_data.region_3d # Find the main 3D region ('WINDOW') in the 3D Viewport region = next((r for r in context.area.regions if r.type == 'WINDOW'), None) # Get mouse position in 3D space # Adjust mouse coordinates to match the main region mouse_coords = ( event.mouse_x - region.x, event.mouse_y - region.y ) view_vector = bpy_extras.view3d_utils.region_2d_to_vector_3d(region, rv3d, mouse_coords) ray_origin = bpy_extras.view3d_utils.region_2d_to_origin_3d(region, rv3d, mouse_coords) # Ensure there is a selected object selected_objects = context.selected_objects if not selected_objects: self.report({'WARNING'}, "No selected object to perform ray cast on.") return mathutils.Vector((1.0, 1.0, 1.0)) # Default white # Use the first selected object obj = selected_objects[0] # Perform a ray cast to find the hit location if obj.type == 'MESH': matrix_world = obj.matrix_world matrix_world_inv = matrix_world.inverted() # Transform ray origin and direction to local space ray_origin_local = matrix_world_inv @ ray_origin ray_target_local = matrix_world_inv @ (ray_origin + view_vector) ray_direction_local = (ray_target_local - ray_origin_local).normalized() # Perform the ray cast in local space result, location, normal, index = obj.ray_cast( ray_origin_local, ray_origin_local + ray_direction_local * 1000 ) print(normal) if result: # Compute the normal in world space normal_world = (matrix_world.to_3x3() @ normal).normalized() print(normal_world) # Map normal components [-1, 1] to [0, 1] for color color = (normal + mathutils.Vector((1.0, 1.0, 1.0))) * 0.5 print(color) return color return mathutils.Vector((1.0, 1.0, 1.0)) # Default white if no hit def brushstroke_callback(scene, depsgraph): active_object = bpy.context.active_object tool_settings = bpy.context.tool_settings # Ensure callback is valid if not active_object or not tool_settings: return elif active_object.mode == 'TEXTURE_PAINT': if tool_settings.image_paint: print("painting ended") apply_color() def register(): bpy.utils.register_class(vPainterOperator) bpy.types.Scene.normal_color_global = bpy.props.FloatVectorProperty(size=3, default=(1.0, 1.0, 1.0)) def unregister(): bpy.utils.unregister_class(vPainterOperator) del bpy.types.Scene.normal_color_global