''' Copyright (C) 2021 - 2023 Akaneyu This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHAmathNTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ''' import ctypes import math import bpy import mathutils import numpy as np from . import app from . import utils class SYMMETRIZE_TEXTURE_OT_use_3d_brush(bpy.types.Operator): """Symmetrize the texture by using 3D brush""" bl_idname = "symmetrize_texture.use_3d_brush" bl_label = "Use 3D Brush" bl_options = {'REGISTER', 'UNDO'} def __init__(self): self.native_lib = None self.lmb = False self.image = None self.image_pixels = None self.eval_object = None self.eval_mesh = None @classmethod def poll(cls, context): return context.mode == 'OBJECT' def modal(self, context, event): session = app.get_session() context.area.tag_redraw() # for updating the 3D view context.tool_settings.image_paint.canvas = context.tool_settings.image_paint.canvas region_pos = [event.mouse_region_x, event.mouse_region_y] if session.resizing_brush: session.brush_position = self.resize_origin else: session.brush_position = region_pos if event.type == 'MOUSEMOVE': if session.resizing_brush: session.brush_size = max(self.initial_brush_size + region_pos[0] - self.resize_origin[0], 1.0) else: if self.lmb: self.process(context) elif event.type == 'LEFTMOUSE': if event.value == 'PRESS': self.lmb = True if session.resizing_brush: session.resizing_brush = False else: self.process(context) elif event.value == 'RELEASE': self.lmb = False elif event.type in ['F']: if not session.resizing_brush: self.resize_origin = region_pos self.initial_brush_size = session.brush_size session.resizing_brush = True elif event.type in ['RIGHTMOUSE', 'ESC', 'RET']: if event.value == 'PRESS': if session.resizing_brush: session.brush_size = self.initial_brush_size session.resizing_brush = False else: self.eval_object.to_mesh_clear() session.brush_position = None session.brush_active = False session.resizing_brush = False bpy.types.SpaceView3D.draw_handler_remove(session.draw_handler, 'WINDOW') self.native_lib.SYMTEX_free() app.unload_native_library(self.native_lib) return {'CANCELLED'} return {'RUNNING_MODAL'} def invoke(self, context, event): session = app.get_session() wm = context.window_manager props = wm.symmetrizetexture_properties if context.area.type != 'VIEW_3D': return {'CANCELLED'} self.native_lib = app.load_native_library() self.native_lib.SYMTEX_init(0) # 3D processor session.brush_position = None session.brush_active = True obj = context.object if not obj or obj.type != 'MESH': return {'CANCELLED'} img = bpy.data.images.get(props.image_preview) if not img or img.use_view_as_render: return {'CANCELLED'} self.image = img depsgraph = context.evaluated_depsgraph_get() self.eval_object = obj.evaluated_get(depsgraph) self.eval_mesh = self.eval_object.to_mesh() obj_mat = obj.matrix_world obj_mat_inv = obj_mat.inverted() region_3d = context.area.spaces.active.region_3d self.native_lib.SYMTEX_setOrthogonal(int(region_3d.view_perspective == 'ORTHO')) pers_mat = region_3d.perspective_matrix pers_mat = pers_mat @ obj_mat self.native_lib.SYMTEX_setPerspectiveMatrix((ctypes.c_float * 16) (*[x for row in pers_mat for x in row])) self.native_lib.SYMTEX_setRegionSize(context.region.width, context.region.height) view_mat_inv = region_3d.view_matrix.inverted() view_pos = obj_mat_inv.to_3x3() @ view_mat_inv.col[3].to_3d() \ + obj_mat_inv.col[3].to_3d() view_dir = obj_mat_inv.to_3x3() @ view_mat_inv.to_3x3() @ mathutils.Vector((0, 0, 1.0)) view_dir.normalize() self.native_lib.SYMTEX_setViewPosition((ctypes.c_float * 3) (view_pos[0], view_pos[1], view_pos[2])) self.native_lib.SYMTEX_setViewDirection((ctypes.c_float * 3) (view_dir[0], view_dir[1], view_dir[2])) num_verts = len(self.eval_mesh.vertices) vert_coords = np.empty(num_verts * 3, dtype=np.float32) vert_norms = np.empty(num_verts * 3, dtype=np.float32) for i, vert in enumerate(self.eval_mesh.vertices): vert_coords[i * 3] = vert.co[0] vert_coords[i * 3 + 1] = vert.co[1] vert_coords[i * 3 + 2] = vert.co[2] vert_norms[i * 3] = vert.normal[0] vert_norms[i * 3 + 1] = vert.normal[1] vert_norms[i * 3 + 2] = vert.normal[2] self.native_lib.SYMTEX_setVertexCoords(vert_coords.ctypes.data_as( ctypes.POINTER(ctypes.c_float)), num_verts) self.native_lib.SYMTEX_setVertexNormals(vert_norms.ctypes.data_as( ctypes.POINTER(ctypes.c_float)), num_verts) num_indices = len(self.eval_mesh.loops) vert_indices = np.empty(num_indices, dtype=np.int32) for i, mesh_loop in enumerate(self.eval_mesh.loops): vert_indices[i] = mesh_loop.vertex_index self.native_lib.SYMTEX_setVertexIndices(vert_indices.ctypes.data_as( ctypes.POINTER(ctypes.c_int)), num_indices) num_uv_coords = len(self.eval_mesh.uv_layers.active.data) uv_coords = np.empty(num_uv_coords * 2, dtype=np.float32) for i, loop_uv in enumerate(self.eval_mesh.uv_layers.active.data): uv_coords[i * 2] = loop_uv.uv[0] uv_coords[i * 2 + 1] = loop_uv.uv[1] self.native_lib.SYMTEX_setUVCoords(uv_coords.ctypes.data_as( ctypes.POINTER(ctypes.c_float)), num_uv_coords) self.eval_mesh.calc_loop_triangles() num_triangles = len(self.eval_mesh.loop_triangles) tri_indices = np.empty(num_triangles * 3, dtype=np.int32) for i, loop_tri in enumerate(self.eval_mesh.loop_triangles): tri_indices[i * 3] = loop_tri.loops[0] tri_indices[i * 3 + 1] = loop_tri.loops[1] tri_indices[i * 3 + 2] = loop_tri.loops[2] self.native_lib.SYMTEX_setTriangles(tri_indices.ctypes.data_as( ctypes.POINTER(ctypes.c_int)), num_triangles) img_width, img_height = self.image.size self.image_pixels = utils.read_pixels_from_image(self.image) self.native_lib.SYMTEX_setImageSize(img_width, img_height) self.native_lib.SYMTEX_setImagePixels(self.image_pixels.ctypes.data_as( ctypes.POINTER(ctypes.c_float))) if props.image_mirror_axis == 'x_axis': mirror_axis = 0 else: mirror_axis = 1 self.native_lib.SYMTEX_setMirrorAxis(mirror_axis) self.native_lib.SYMTEX_prepare() session.draw_handler = bpy.types.SpaceView3D.draw_handler_add( app.draw_handler, (), 'WINDOW', 'POST_PIXEL') context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} def process(self, context): session = app.get_session() wm = context.window_manager props = wm.symmetrizetexture_properties self.native_lib.SYMTEX_setBrushSize(ctypes.c_float(session.brush_size)) self.native_lib.SYMTEX_setBrushStrength(ctypes.c_float(props.brush_strength)) if props.brush_falloff == 'smooth': falloff_type = 0 else: falloff_type = 9 self.native_lib.SYMTEX_setBrushFalloffType(falloff_type) pos = session.brush_position self.native_lib.SYMTEX_processStroke((ctypes.c_float * 2)(*pos)) utils.write_pixels_to_image(self.image, self.image_pixels, False) self.image.update() class SYMMETRIZE_TEXTURE_OT_save_image(bpy.types.Operator): """Save the image""" bl_idname = "image_layers_node.save_image" bl_label = "Save Image" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return context.mode == 'OBJECT' def execute(self, context): wm = context.window_manager props = wm.symmetrizetexture_properties img = bpy.data.images.get(props.image_preview) if not img or img.use_view_as_render: return {'CANCELLED'} if img.packed_files: img.pack() elif img.filepath: img.save() return {'FINISHED'} class SYMMETRIZE_TEXTURE_OT_mirrored_copy(bpy.types.Operator): """Create a Mirrored copy of image""" bl_idname = "symmetrize_texture.mirrored_copy" bl_label = "Mirrored Copy" bl_options = {'REGISTER', 'UNDO'} def __init__(self): pass @classmethod def poll(cls, context): return context.area.spaces.active.mode != 'UV' \ and context.area.spaces.active.image != None \ and not context.area.spaces.active.image.use_view_as_render def modal(self, context, event): session = app.get_session() wm = context.window_manager props = wm.symmetrizetexture_properties context.area.tag_redraw() #context.tool_settings.image_paint.canvas = context.tool_settings.image_paint.canvas region_pos = [event.mouse_region_x, event.mouse_region_y] target_x, target_y = context.region.view2d.region_to_view(*region_pos) if props.image_mirror_axis == 'x_axis': session.direction = target_x - 0.5 else: session.direction = target_y - 0.5 if event.type in ['LEFTMOUSE']: self.process(context) session.selecting_direction = False bpy.types.SpaceImageEditor.draw_handler_remove(session.draw_handler, 'WINDOW') return {'FINISHED'} elif event.type in ['RIGHTMOUSE', 'ESC', 'RET']: session.selecting_direction = False bpy.types.SpaceImageEditor.draw_handler_remove(session.draw_handler, 'WINDOW') return {'CANCELLED'} return {'RUNNING_MODAL'} def invoke(self, context, event): session = app.get_session() if context.area.type != 'IMAGE_EDITOR': return {'CANCELLED'} session.selecting_direction = True session.draw_handler = bpy.types.SpaceImageEditor.draw_handler_add( app.draw_handler, (), 'WINDOW', 'POST_PIXEL') context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} def process(self, context): session = app.get_session() wm = context.window_manager props = wm.symmetrizetexture_properties img = context.area.spaces.active.image img_width, img_height = img.size img_pixels = utils.read_pixels_from_image(img) half_img_width = img_width / 2 half_img_height = img_height / 2 x_mid_1, x_mid_2 = int(math.floor(half_img_width)), int(math.ceil(half_img_width)) y_mid_1, y_mid_2 = int(math.floor(half_img_height)), int(math.ceil(half_img_height)) if props.image_mirror_axis == 'x_axis': if session.direction > 0: mirror_pixels = img_pixels[:, :x_mid_1] img_pixels[:, x_mid_2:] = np.fliplr(mirror_pixels) else: mirror_pixels = img_pixels[:, x_mid_2:] img_pixels[:, :x_mid_1] = np.fliplr(mirror_pixels) else: if session.direction > 0: mirror_pixels = img_pixels[:y_mid_1] img_pixels[y_mid_2:] = np.flipud(mirror_pixels) else: mirror_pixels = img_pixels[y_mid_2:] img_pixels[:y_mid_1] = np.flipud(mirror_pixels) utils.write_pixels_to_image(img, img_pixels, False) img.update() class SYMMETRIZE_TEXTURE_OT_use_2d_brush(bpy.types.Operator): """Symmetrize the texture by using 2D brush""" bl_idname = "symmetrize_texture.use_2d_brush" bl_label = "Use 2D Brush" bl_options = {'REGISTER', 'UNDO'} def __init__(self): self.native_lib = None self.lmb = False self.image = None self.image_pixels = None @classmethod def poll(cls, context): return context.area.spaces.active.mode != 'UV' \ and context.area.spaces.active.image != None \ and not context.area.spaces.active.image.use_view_as_render def modal(self, context, event): session = app.get_session() context.area.tag_redraw() #context.tool_settings.image_paint.canvas = context.tool_settings.image_paint.canvas region_pos = [event.mouse_region_x, event.mouse_region_y] if session.resizing_brush: session.brush_position = self.resize_origin else: session.brush_position = region_pos if event.type == 'MOUSEMOVE': if session.resizing_brush: session.brush_size = max(self.initial_brush_size + region_pos[0] - self.resize_origin[0], 1.0) else: if self.lmb: self.process(context) else: pass #self.process(context, False) elif event.type == 'LEFTMOUSE': if event.value == 'PRESS': self.lmb = True if session.resizing_brush: session.resizing_brush = False else: self.process(context) elif event.value == 'RELEASE': self.lmb = False elif event.type in ['F']: if not session.resizing_brush: self.resize_origin = region_pos self.initial_brush_size = session.brush_size session.resizing_brush = True elif event.type in ['RIGHTMOUSE', 'ESC', 'RET']: if event.value == 'PRESS': if session.resizing_brush: session.brush_size = self.initial_brush_size session.resizing_brush = False else: session.brush_position = None session.brush_active = False session.resizing_brush = False bpy.types.SpaceImageEditor.draw_handler_remove(session.draw_handler, 'WINDOW') self.native_lib.SYMTEX_free() app.unload_native_library(self.native_lib) return {'CANCELLED'} return {'RUNNING_MODAL'} def invoke(self, context, event): session = app.get_session() wm = context.window_manager props = wm.symmetrizetexture_properties if context.area.type != 'IMAGE_EDITOR': return {'CANCELLED'} self.native_lib = app.load_native_library() self.native_lib.SYMTEX_init(1) # 2D processor session.brush_position = None session.brush_active = True self.image = context.area.spaces.active.image img_width, img_height = self.image.size self.image_pixels = utils.read_pixels_from_image(self.image) self.native_lib.SYMTEX_setImageSize(img_width, img_height) self.native_lib.SYMTEX_setImagePixels(self.image_pixels.ctypes.data_as( ctypes.POINTER(ctypes.c_float))) if props.image_mirror_axis == 'x_axis': mirror_axis = 0 else: mirror_axis = 1 self.native_lib.SYMTEX_setMirrorAxis(mirror_axis) self.native_lib.SYMTEX_prepare() session.draw_handler = bpy.types.SpaceImageEditor.draw_handler_add( app.draw_handler, (), 'WINDOW', 'POST_PIXEL') context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} def process(self, context): session = app.get_session() wm = context.window_manager props = wm.symmetrizetexture_properties view_x, view_y = context.region.view2d.region_to_view(*session.brush_position) radius_x1, dummy = context.region.view2d.region_to_view(0, 0) radius_x2, dummy = context.region.view2d.region_to_view(session.brush_size, 0) radius = radius_x2 - radius_x1 self.native_lib.SYMTEX_setBrushSize(ctypes.c_float(radius)) self.native_lib.SYMTEX_setBrushStrength(ctypes.c_float(props.brush_strength)) if props.brush_falloff == 'smooth': falloff_type = 0 else: falloff_type = 9 self.native_lib.SYMTEX_setBrushFalloffType(falloff_type) self.native_lib.SYMTEX_processStroke((ctypes.c_float * 2)(view_x, view_y)) utils.write_pixels_to_image(self.image, self.image_pixels, False) self.image.update()