From 7ed6d0228c09b549b3262f52fe394888909fa043 Mon Sep 17 00:00:00 2001 From: x-o-i-o-x <103693092+x-o-i-o-x@users.noreply.github.com> Date: Sat, 4 Oct 2025 21:53:00 +0200 Subject: [PATCH] paintListener implementation started --- README.md | 5 ++ paintListener.py | 150 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 153 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f47bb64..6b663a6 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,7 @@ # virtualPainter +Building: + +cd "C:\Program Files\Blender Foundation\Blender 4.5" + +.\blender --command extension build --source-dir C:\Users\MegaA\OneDrive\Documents\Blender\add-on_dev\virtualPainter --output-dir C:\Users\MegaA\OneDrive\Documents\Blender\add-on_dev\test_builds \ No newline at end of file diff --git a/paintListener.py b/paintListener.py index dc5d831..860c8fa 100644 --- a/paintListener.py +++ b/paintListener.py @@ -1,7 +1,153 @@ import bpy +import mathutils +import bpy_extras +from bpy_extras import view3d_utils + +image_name = "vPainter_buffer" +image_width = 1024 +image_height = 1024 + +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 +# mouse_coords = (event.mouse_region_x, event.mouse_region_y) + # 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) + +# print(mouse_coords) +# print(view_vector) +# print(ray_origin) + + # Perform a ray cast to find the hit location +# obj = context.object + + # 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] + + 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(f"Ray cast result: {result}, Location (local): {location_local}, Normal (local): {normal_local}") + 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(): - pass + bpy.utils.register_class(vPainterOperator) def unregister(): - pass \ No newline at end of file + bpy.utils.unregister_class(vPainterOperator) \ No newline at end of file